#include "BasicOutputHandler.hpp" #include "AdvancedOutput.hpp" #include "SimpleOutput.hpp" #include #include #include #include #include #include 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; 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)); } 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)); } 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"); } 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)); } 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"); } 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)); } void OBSRecordStopping(void *data, calldata_t * /* params */) { BasicOutputHandler *output = static_cast(data); QMetaObject::invokeMethod(output->main, "RecordStopping"); } 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; } 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"); } 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)); } void OBSReplayBufferStopping(void *data, calldata_t * /* params */) { BasicOutputHandler *output = static_cast(data); QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); } 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(); } bool return_first_id(void *data, const char *id) { const char **output = (const char **)data; *output = id; return false; } 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; } 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 service = main_->GetService(); OBSDataAutoRelease settings = obs_service_get_settings(service); auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo") && 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; } 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; } 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); } 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")); std::optional extraCanvasUUID; const char *uuid = config_get_string(main->Config(), "Stream1", "MultitrackExtraCanvas"); if (uuid && *uuid) { extraCanvasUUID = uuid; } 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, extraCanvasUUID); } 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); }