From 6eec0cbed0529f75a62d1884af566ed10328645b Mon Sep 17 00:00:00 2001 From: Roy Shilkrot Date: Wed, 15 Nov 2023 17:08:10 -0500 Subject: [PATCH] Add aggregate to empty and binary data (#52) * Add agg to empty and binary data * lint * lint again * moar lint --- src/parsers/CMakeLists.txt | 10 +- src/parsers/binary-data.cpp | 82 +++++++++ src/parsers/parsers.h | 6 + src/request-data.cpp | 227 +++++++++++++++--------- src/request-data.h | 7 +- src/string-util.h | 41 +++++ src/ui/RequestBuilder.cpp | 235 +++++++++++++++---------- src/ui/requestbuilder.ui | 115 +++++++----- src/url-source.cpp | 342 ++++++++++++++++++++++-------------- 9 files changed, 705 insertions(+), 360 deletions(-) create mode 100644 src/parsers/binary-data.cpp create mode 100644 src/string-util.h diff --git a/src/parsers/CMakeLists.txt b/src/parsers/CMakeLists.txt index fd51be4..49a78ae 100644 --- a/src/parsers/CMakeLists.txt +++ b/src/parsers/CMakeLists.txt @@ -1,4 +1,12 @@ -target_sources(${CMAKE_PROJECT_NAME} PRIVATE jsonpointer.cpp jsonpath.cpp regex.cpp xml.cpp errors.cpp html.cpp) +target_sources( + ${CMAKE_PROJECT_NAME} + PRIVATE jsonpointer.cpp + jsonpath.cpp + regex.cpp + xml.cpp + errors.cpp + html.cpp + binary-data.cpp) # on linux, disable conversion errors if(UNIX AND NOT APPLE) diff --git a/src/parsers/binary-data.cpp b/src/parsers/binary-data.cpp new file mode 100644 index 0000000..cccf964 --- /dev/null +++ b/src/parsers/binary-data.cpp @@ -0,0 +1,82 @@ +#include "errors.h" +#include "request-data.h" +#include "plugin-support.h" +#include "string-util.h" + +#include + +#include +#include +#include +#include + +std::string save_to_temp_file(const std::vector &data, const std::string &extension) +{ + // check if the config folder exists if it doesn't exist, create it. + char *config_path = obs_module_config_path(""); + if (!std::filesystem::exists(config_path)) { + if (!std::filesystem::create_directory(config_path)) { + obs_log(LOG_ERROR, "Failed to create config directory %s", config_path); + bfree(config_path); + return ""; + } + } + bfree(config_path); + + // append the extension to the file name + std::string file_name = "temp." + extension; + char *temp_file_path = obs_module_config_path(file_name.c_str()); + + obs_log(LOG_INFO, "save to temp file %s, %d bytes", temp_file_path, data.size()); + + std::ofstream temp_file(temp_file_path, std::ios::binary); + bfree(temp_file_path); + temp_file.write((const char *)data.data(), data.size()); + temp_file.close(); + return std::string(temp_file_path); +} + +struct request_data_handler_response parse_image_data(struct request_data_handler_response response, + const url_source_request_data *request_data) +{ + UNUSED_PARAMETER(request_data); + + // find the image type from the content type on the response.headers map + std::string content_type = response.headers["content-type"]; + std::string image_type = content_type.substr(content_type.find("/") + 1); + + // if the image type is not supported, return an error + if (image_type != "png" && image_type != "jpg" && image_type != "jpeg" && + image_type != "gif") { + return make_fail_parse_response("Unsupported image type: " + image_type); + } + + // save the image to a temporary file + std::string temp_file_path = save_to_temp_file(response.body_bytes, image_type); + response.body = temp_file_path; + + return response; +} + +struct request_data_handler_response parse_audio_data(struct request_data_handler_response response, + const url_source_request_data *request_data) +{ + UNUSED_PARAMETER(request_data); + + // find the audio type from the content type on the response.headers map + std::string content_type = response.headers["content-type"]; + std::string audio_type = content_type.substr(content_type.find("/") + 1); + audio_type = trim(audio_type); + + // if the audio type is not supported, return an error + if (!(audio_type == "mp3" || audio_type == "mpeg" || audio_type == "wav" || + audio_type == "ogg" || audio_type == "flac" || audio_type == "aac")) { + return make_fail_parse_response("Unsupported audio type: " + audio_type); + } + + // save the audio to a temporary file + std::string temp_file_path = save_to_temp_file(response.body_bytes, audio_type); + response.body = temp_file_path; + + return response; +} diff --git a/src/parsers/parsers.h b/src/parsers/parsers.h index f07300f..74e7148 100644 --- a/src/parsers/parsers.h +++ b/src/parsers/parsers.h @@ -22,6 +22,12 @@ struct request_data_handler_response parse_xml(struct request_data_handler_respo struct request_data_handler_response parse_html(struct request_data_handler_response response, const url_source_request_data *request_data); +struct request_data_handler_response parse_image_data(struct request_data_handler_response response, + const url_source_request_data *request_data); + +struct request_data_handler_response parse_audio_data(struct request_data_handler_response response, + const url_source_request_data *request_data); + struct request_data_handler_response parse_xml_by_xquery(struct request_data_handler_response response, const url_source_request_data *request_data); diff --git a/src/request-data.cpp b/src/request-data.cpp index 960337b..5dca6e4 100644 --- a/src/request-data.cpp +++ b/src/request-data.cpp @@ -3,11 +3,15 @@ #include "request-data.h" #include "plugin-support.h" #include "parsers/parsers.h" +#include "string-util.h" #include #include #include #include +#include +#include +#include #include #include @@ -15,6 +19,8 @@ #include #include +#define URL_SOURCE_AGG_BUFFER_MAX_SIZE 1024 + static const std::string USER_AGENT = std::string(PLUGIN_NAME) + "/" + std::string(PLUGIN_VERSION); std::size_t writeFunctionStdString(void *ptr, std::size_t size, size_t nmemb, std::string *data) @@ -31,6 +37,35 @@ std::size_t writeFunctionUint8Vector(void *ptr, std::size_t size, size_t nmemb, return size * nmemb; } +size_t header_callback(char *buffer, size_t size, size_t nitems, void *userdata) +{ + std::map *headers = + static_cast *>(userdata); + size_t real_size = size * nitems; + std::string header_line(buffer, real_size); + + // Find the colon in the line + auto pos = header_line.find(':'); + if (pos != std::string::npos) { + std::string key = header_line.substr(0, pos); + std::string value = header_line.substr(pos + 1); + + // Remove potential carriage return + if (!value.empty() && value.back() == '\r') { + value.pop_back(); + } + + // Convert key to lowercase + std::transform(key.begin(), key.end(), key.begin(), + [](unsigned char c) { return std::tolower(c); }); + + // Store the header + (*headers)[key] = value; + } + + return real_size; +} + struct request_data_handler_response request_data_handler(url_source_request_data *request_data) { struct request_data_handler_response response; @@ -73,7 +108,21 @@ struct request_data_handler_response request_data_handler(url_source_request_dat } curl_easy_setopt(curl, CURLOPT_URL, request_data->url.c_str()); curl_easy_setopt(curl, CURLOPT_USERAGENT, USER_AGENT.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunctionStdString); + + std::string responseBody; + std::vector responseBodyUint8; + + // if the request is for textual data write to string + if (request_data->output_type == "JSON" || + request_data->output_type == "XML (XPath)" || + request_data->output_type == "XML (XQuery)" || + request_data->output_type == "HTML" || request_data->output_type == "Text") { + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBody); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunctionStdString); + } else { + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBodyUint8); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunctionUint8Vector); + } if (request_data->headers.size() > 0) { // Add request headers @@ -94,8 +143,16 @@ struct request_data_handler_response request_data_handler(url_source_request_dat obs_get_source_by_name(request_data->obs_text_source.c_str())); const char *text = obs_data_get_string(sourceSettings, "text"); obs_data_release(sourceSettings); - if (request_data->obs_text_source_skip_if_empty) { - if (text == NULL || text[0] == '\0') { + if (text == NULL || text[0] == '\0') { + if (request_data->aggregate_to_empty && + !request_data->aggregate_to_empty_buffer.empty()) { + // empty input found, use the aggregate buffer if it's not empty + obs_log(LOG_INFO, + "OBS text source is empty, using aggregate buffer %s", + request_data->aggregate_to_empty_buffer.c_str()); + json["input"] = request_data->aggregate_to_empty_buffer; + request_data->aggregate_to_empty_buffer = ""; + } else if (request_data->obs_text_source_skip_if_empty) { // Return an error response response.error_message = "OBS text source is empty, skipping was requested"; @@ -113,10 +170,31 @@ struct request_data_handler_response request_data_handler(url_source_request_dat } request_data->last_obs_text_source_value = text; } + if (request_data->aggregate_to_empty && text != NULL && text[0] != '\0') { + // aggregate to empty is requested and the text is not empty + // trim the text and add it to the aggregate buffer + std::string textStr = text; + request_data->aggregate_to_empty_buffer += trim(textStr); + // if the buffer is larger than the limit, remove the first part + if (request_data->aggregate_to_empty_buffer.size() > + URL_SOURCE_AGG_BUFFER_MAX_SIZE) { + request_data->aggregate_to_empty_buffer.erase( + 0, request_data->aggregate_to_empty_buffer.size() - + URL_SOURCE_AGG_BUFFER_MAX_SIZE); + } + response.error_message = + "Aggregate to empty is requested, skipping"; + response.status_code = URL_SOURCE_REQUEST_BENIGN_ERROR_CODE; + return response; + } + // if one of the headers is Content-Type application/json, make sure the text is JSONified std::string textStr = text; for (auto header : request_data->headers) { - if (header.first == "Content-Type" && + // check if the header is Content-Type case insensitive using regex + std::regex header_regex("content-type", + std::regex_constants::icase); + if (std::regex_search(header.first, header_regex) && header.second == "application/json") { nlohmann::json tmp = text; textStr = tmp.dump(); @@ -125,7 +203,10 @@ struct request_data_handler_response request_data_handler(url_source_request_dat break; } } - json["input"] = textStr; + if (!json.contains("input")) { + // only set the input if it wasn't set by aggregate-to-empty + json["input"] = textStr; + } } // Replace the {input} placeholder with the source text @@ -195,8 +276,9 @@ struct request_data_handler_response request_data_handler(url_source_request_dat curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); } - std::string responseBody; - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBody); + std::map headers; + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headers); // Send the request CURLcode code = curl_easy_perform(curl); @@ -211,9 +293,14 @@ struct request_data_handler_response request_data_handler(url_source_request_dat } response.body = responseBody; + response.body_bytes = responseBodyUint8; response.status_code = URL_SOURCE_REQUEST_SUCCESS; + response.headers = headers; } + if (!response.body.empty()) + obs_log(LOG_INFO, "Response Body: %s", response.body.c_str()); + // Parse the response if (request_data->output_type == "JSON") { if (request_data->output_json_path != "") { @@ -232,6 +319,10 @@ struct request_data_handler_response request_data_handler(url_source_request_dat response = parse_html(response, request_data); } else if (request_data->output_type == "Text") { response = parse_regex(response, request_data); + } else if (request_data->output_type == "Image (data)") { + response = parse_image_data(response, request_data); + } else if (request_data->output_type == "Audio (data)") { + response = parse_audio_data(response, request_data); } else { obs_log(LOG_INFO, "Invalid output type"); // Return an error response @@ -241,6 +332,9 @@ struct request_data_handler_response request_data_handler(url_source_request_dat return responseFail; } + if (!response.body.empty()) + obs_log(LOG_INFO, "Response Body after parse: %s", response.body.c_str()); + // If output regex is set - use it to format the output in response.body_parts_parsed if (!request_data->post_process_regex.empty()) { try { @@ -283,6 +377,7 @@ std::string serialize_request_data(url_source_request_data *request_data) json["obs_text_source"] = request_data->obs_text_source; json["obs_text_source_skip_if_empty"] = request_data->obs_text_source_skip_if_empty; json["obs_text_source_skip_if_same"] = request_data->obs_text_source_skip_if_same; + json["aggregate_to_empty"] = request_data->aggregate_to_empty; // SSL options json["ssl_client_cert_file"] = request_data->ssl_client_cert_file; json["ssl_client_key_file"] = request_data->ssl_client_key_file; @@ -322,98 +417,60 @@ url_source_request_data unserialize_request_data(std::string serialized_request_ json = nlohmann::json::parse(serialized_request_data); request_data.url = json["url"].get(); - if (json.contains("url_or_file")) { - request_data.url_or_file = json["url_or_file"].get(); - } else { - request_data.url_or_file = "url"; - } + request_data.url_or_file = json.value("url_or_file", "url"); request_data.method = json["method"].get(); request_data.body = json["body"].get(); - if (json.contains("obs_text_source")) { - request_data.obs_text_source = json["obs_text_source"].get(); - } else { - request_data.obs_text_source = ""; - } - if (json.contains("obs_text_source_skip_if_empty")) { - request_data.obs_text_source_skip_if_empty = - json["obs_text_source_skip_if_empty"].get(); - } else { - request_data.obs_text_source_skip_if_empty = false; - } - if (json.contains("obs_text_source_skip_if_same")) { - request_data.obs_text_source_skip_if_same = - json["obs_text_source_skip_if_same"].get(); - } else { - request_data.obs_text_source_skip_if_same = false; - } + request_data.obs_text_source = json.value("obs_text_source", ""); + request_data.obs_text_source_skip_if_empty = + json.value("obs_text_source_skip_if_empty", false); + request_data.obs_text_source_skip_if_same = + json.value("obs_text_source_skip_if_same", false); + request_data.aggregate_to_empty = json.value("aggregate_to_empty", false); // SSL options - if (json.contains("ssl_client_cert_file")) { - request_data.ssl_client_cert_file = - json["ssl_client_cert_file"].get(); - } - if (json.contains("ssl_client_key_file")) { - request_data.ssl_client_key_file = - json["ssl_client_key_file"].get(); - } - if (json.contains("ssl_client_key_pass")) { - request_data.ssl_client_key_pass = - json["ssl_client_key_pass"].get(); - } + request_data.ssl_client_cert_file = json.value("ssl_client_cert_file", ""); + request_data.ssl_client_key_file = json.value("ssl_client_key_file", ""); + request_data.ssl_client_key_pass = json.value("ssl_client_key_pass", ""); + // Output parsing options - request_data.output_type = json["output_type"].get(); - if (json.contains("output_json_path")) { - request_data.output_json_path = json["output_json_path"].get(); - if (request_data.output_json_path != "" && - request_data.output_json_path[0] == '/') { - obs_log(LOG_WARNING, - "JSON pointer detected in JSON Path. Translating to JSON " - "path."); - // this is a json pointer - translate by adding a "$" prefix and replacing all - // "/"s with "." - request_data.output_json_path = - "$." + request_data.output_json_path.substr(1); - std::replace(request_data.output_json_path.begin(), - request_data.output_json_path.end(), '/', '.'); - } + request_data.output_type = json.value("output_type", "Text"); + request_data.output_json_path = json.value("output_json_path", ""); + if (request_data.output_json_path != "" && + request_data.output_json_path[0] == '/') { + obs_log(LOG_WARNING, + "JSON pointer detected in JSON Path. Translating to JSON " + "path."); + // this is a json pointer - translate by adding a "$" prefix and replacing all + // "/"s with "." + request_data.output_json_path = + "$." + request_data.output_json_path.substr(1); + std::replace(request_data.output_json_path.begin(), + request_data.output_json_path.end(), '/', '.'); } - if (json.contains("output_json_pointer")) { - request_data.output_json_pointer = - json["output_json_pointer"].get(); - if (request_data.output_json_pointer != "" && - request_data.output_json_pointer[0] == '$') { - obs_log(LOG_WARNING, - "JSON path detected in JSON Pointer. Translating to JSON " - "pointer."); - // this is a json path - translate by replacing all "."s with "/"s - std::replace(request_data.output_json_pointer.begin(), - request_data.output_json_pointer.end(), '.', '/'); - request_data.output_json_pointer = - "/" + request_data.output_json_pointer; - } + request_data.output_json_pointer = json.value("output_json_pointer", ""); + if (request_data.output_json_pointer != "" && + request_data.output_json_pointer[0] == '$') { + obs_log(LOG_WARNING, + "JSON path detected in JSON Pointer. Translating to JSON " + "pointer."); + // this is a json path - translate by replacing all "."s with "/"s + std::replace(request_data.output_json_pointer.begin(), + request_data.output_json_pointer.end(), '.', '/'); + request_data.output_json_pointer = "/" + request_data.output_json_pointer; } request_data.output_xpath = json["output_xpath"].get(); request_data.output_xquery = json["output_xquery"].get(); request_data.output_regex = json["output_regex"].get(); request_data.output_regex_flags = json["output_regex_flags"].get(); request_data.output_regex_group = json["output_regex_group"].get(); - if (json.contains("output_cssselector")) - request_data.output_cssselector = - json["output_cssselector"].get(); + request_data.output_cssselector = json.value("output_cssselector", ""); // postprocess options - if (json.contains("post_process_regex")) { - request_data.post_process_regex = - json["post_process_regex"].get(); - } - if (json.contains("post_process_regex_is_replace")) { - request_data.post_process_regex_is_replace = - json["post_process_regex_is_replace"].get(); - } - if (json.contains("post_process_regex_replace")) { - request_data.post_process_regex_replace = - json["post_process_regex_replace"].get(); - } + request_data.post_process_regex = json.value("post_process_regex", ""); + request_data.post_process_regex_is_replace = + json.value("post_process_regex_is_replace", false); + request_data.post_process_regex_replace = + json.value("post_process_regex_replace", ""); nlohmann::json headers_json = json["headers"]; for (auto header : headers_json.items()) { diff --git a/src/request-data.h b/src/request-data.h index 3490904..26e96d6 100644 --- a/src/request-data.h +++ b/src/request-data.h @@ -18,6 +18,8 @@ struct url_source_request_data { std::string obs_text_source; bool obs_text_source_skip_if_empty; bool obs_text_source_skip_if_same; + bool aggregate_to_empty; + std::string aggregate_to_empty_buffer; std::string last_obs_text_source_value; // SSL options std::string ssl_client_cert_file; @@ -51,6 +53,8 @@ struct url_source_request_data { obs_text_source = std::string(""); obs_text_source_skip_if_empty = false; obs_text_source_skip_if_same = false; + aggregate_to_empty = false; + aggregate_to_empty_buffer = std::string(""); last_obs_text_source_value = std::string(""); ssl_verify_peer = false; headers = {}; @@ -68,10 +72,11 @@ struct url_source_request_data { struct request_data_handler_response { std::string body; + std::vector body_bytes; nlohmann::json body_json; std::string content_type; std::vector body_parts_parsed; - std::vector> headers; + std::map headers; int status_code; std::string status_message; std::string error_message; diff --git a/src/string-util.h b/src/string-util.h new file mode 100644 index 0000000..1e1a042 --- /dev/null +++ b/src/string-util.h @@ -0,0 +1,41 @@ +#ifndef STRING_UTIL_H +#define STRING_UTIL_H + +#ifdef __cplusplus + +#include +#include +#include +#include + +namespace { + +// Trim from start (left) +static inline std::string <rim(std::string &s) +{ + s.erase(s.begin(), std::find_if(s.begin(), s.end(), + [](unsigned char ch) { return !std::isspace(ch); })); + return s; +} + +// Trim from end (right) +static inline std::string &rtrim(std::string &s) +{ + s.erase(std::find_if(s.rbegin(), s.rend(), + [](unsigned char ch) { return !std::isspace(ch); }) + .base(), + s.end()); + return s; +} + +// Trim from both ends +static inline std::string &trim(std::string &s) +{ + return ltrim(rtrim(s)); +} + +} + +#endif + +#endif diff --git a/src/ui/RequestBuilder.cpp b/src/ui/RequestBuilder.cpp index f3846cc..7482371 100644 --- a/src/ui/RequestBuilder.cpp +++ b/src/ui/RequestBuilder.cpp @@ -2,18 +2,29 @@ #include "RequestBuilder.h" #include "ui_requestbuilder.h" #include "CollapseButton.h" +#include "plugin-support.h" #include void set_form_row_visibility(QFormLayout *layout, QWidget *widget, bool visible) { + if (layout == nullptr) { + return; + } + if (widget == nullptr) { + return; + } for (int i = 0; i < layout->rowCount(); i++) { - if (layout->itemAt(i, QFormLayout::FieldRole)->widget() == widget) { + QLayoutItem *item = layout->itemAt(i, QFormLayout::FieldRole); + if (item == nullptr) { + continue; + } + if (item->widget() == widget) { #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) layout->setRowVisible(i, visible); #else + item->widget()->setVisible(visible); layout->itemAt(i, QFormLayout::LabelRole)->widget()->setVisible(visible); - layout->itemAt(i, QFormLayout::FieldRole)->widget()->setVisible(visible); #endif return; } @@ -150,6 +161,7 @@ RequestBuilder::RequestBuilder(url_source_request_data *request_data, } ui->obsTextSourceEnabledCheckBox->setChecked(request_data->obs_text_source_skip_if_empty); ui->obsTextSourceSkipSameCheckBox->setChecked(request_data->obs_text_source_skip_if_same); + ui->aggToEmpty->setChecked(request_data->aggregate_to_empty); ui->bodyTextEdit->setText(QString::fromStdString(request_data->body)); auto setVisibilityOfBody = [=]() { @@ -177,14 +189,13 @@ RequestBuilder::RequestBuilder(url_source_request_data *request_data, ui->outputRegexGroupLineEdit->setText( QString::fromStdString(request_data->output_regex_group)); ui->cssSelectorLineEdit->setText(QString::fromStdString(request_data->output_cssselector)); - auto setVisibilityOfOutputParsingOptions = [=]() { // Hide all output parsing options for (const auto &widget : {ui->outputJSONPathLineEdit, ui->outputXPathLineEdit, ui->outputXQueryLineEdit, ui->outputRegexLineEdit, ui->outputRegexFlagsLineEdit, ui->outputRegexGroupLineEdit, ui->outputJSONPointerLineEdit, - ui->cssSelectorLineEdit}) { + ui->cssSelectorLineEdit, ui->postProcessRegexLineEdit}) { set_form_row_visibility(ui->formOutputParsing, widget, false); } @@ -194,15 +205,23 @@ RequestBuilder::RequestBuilder(url_source_request_data *request_data, true); set_form_row_visibility(ui->formOutputParsing, ui->outputJSONPointerLineEdit, true); + set_form_row_visibility(ui->formOutputParsing, ui->postProcessRegexLineEdit, + true); } else if (ui->outputTypeComboBox->currentText() == "XML (XPath)") { set_form_row_visibility(ui->formOutputParsing, ui->outputXPathLineEdit, true); + set_form_row_visibility(ui->formOutputParsing, ui->postProcessRegexLineEdit, + true); } else if (ui->outputTypeComboBox->currentText() == "XML (XQuery)") { set_form_row_visibility(ui->formOutputParsing, ui->outputXQueryLineEdit, true); + set_form_row_visibility(ui->formOutputParsing, ui->postProcessRegexLineEdit, + true); } else if (ui->outputTypeComboBox->currentText() == "HTML") { set_form_row_visibility(ui->formOutputParsing, ui->cssSelectorLineEdit, true); + set_form_row_visibility(ui->formOutputParsing, ui->postProcessRegexLineEdit, + true); } else if (ui->outputTypeComboBox->currentText() == "Text") { set_form_row_visibility(ui->formOutputParsing, ui->outputRegexLineEdit, true); @@ -210,6 +229,8 @@ RequestBuilder::RequestBuilder(url_source_request_data *request_data, true); set_form_row_visibility(ui->formOutputParsing, ui->outputRegexGroupLineEdit, true); + set_form_row_visibility(ui->formOutputParsing, ui->postProcessRegexLineEdit, + true); } }; @@ -257,6 +278,7 @@ RequestBuilder::RequestBuilder(url_source_request_data *request_data, ui->obsTextSourceEnabledCheckBox->isChecked(); request_data_for_saving->obs_text_source_skip_if_same = ui->obsTextSourceSkipSameCheckBox->isChecked(); + request_data_for_saving->aggregate_to_empty = ui->aggToEmpty->isChecked(); // Save the SSL certificate file request_data_for_saving->ssl_client_cert_file = @@ -319,101 +341,126 @@ RequestBuilder::RequestBuilder(url_source_request_data *request_data, url_source_request_data *request_data_test = new url_source_request_data; saveSettingsToRequestData(request_data_test); - // Send the request - struct request_data_handler_response response = - request_data_handler(request_data_test); + // disable the send button + ui->sendButton->setEnabled(false); + + // Create a request_data_handler function that will be called by the thread + auto request_data_handler_ex = [this](url_source_request_data *request_data) { + obs_log(LOG_INFO, "Sending request to %s", request_data->url.c_str()); + struct request_data_handler_response response = + request_data_handler(request_data); + + if (response.status_code != URL_SOURCE_REQUEST_SUCCESS) { + // Show the error message label + this->ui->errorMessageLabel->setText( + QString::fromStdString(response.error_message)); + this->ui->errorMessageLabel->setVisible(true); + // enable the send button + this->ui->sendButton->setEnabled(true); + return; + } - if (response.status_code != URL_SOURCE_REQUEST_SUCCESS) { - // Show the error message label - ui->errorMessageLabel->setText( - QString::fromStdString(response.error_message)); - ui->errorMessageLabel->setVisible(true); - return; - } + obs_log(LOG_INFO, "Response status code: %d", response.status_code); + + // Display the response + QDialog *responseDialog = new QDialog(this); + responseDialog->setWindowTitle("Response"); + QVBoxLayout *responseLayout = new QVBoxLayout; + responseDialog->setLayout(responseLayout); + responseDialog->setMinimumWidth(500); + responseDialog->setMinimumHeight(300); + responseDialog->show(); + responseDialog->raise(); + responseDialog->activateWindow(); + responseDialog->setModal(true); + + // show request URL + QGroupBox *requestGroupBox = new QGroupBox("Request"); + responseLayout->addWidget(requestGroupBox); + QVBoxLayout *requestLayout = new QVBoxLayout; + requestGroupBox->setLayout(requestLayout); + QScrollArea *requestUrlScrollArea = new QScrollArea; + QLabel *requestUrlLabel = + new QLabel(QString::fromStdString(response.request_url)); + requestUrlScrollArea->setWidget(requestUrlLabel); + requestLayout->addWidget(requestUrlScrollArea); + + // if there's a request body, add it to the dialog + if (response.request_body != "") { + QGroupBox *requestBodyGroupBox = new QGroupBox("Request Body"); + responseLayout->addWidget(requestBodyGroupBox); + QVBoxLayout *requestBodyLayout = new QVBoxLayout; + requestBodyGroupBox->setLayout(requestBodyLayout); + // Add scroll area for the request body + QScrollArea *requestBodyScrollArea = new QScrollArea; + QLabel *requestLabel = + new QLabel(QString::fromStdString(response.request_body)); + // Wrap the text + requestLabel->setWordWrap(true); + // Set the label as the scroll area's widget + requestBodyScrollArea->setWidget(requestLabel); + requestBodyLayout->addWidget(requestBodyScrollArea); + } - // Display the response - QDialog *responseDialog = new QDialog(this); - responseDialog->setWindowTitle("Response"); - QVBoxLayout *responseLayout = new QVBoxLayout; - responseDialog->setLayout(responseLayout); - responseDialog->setMinimumWidth(500); - responseDialog->setMinimumHeight(300); - responseDialog->show(); - responseDialog->raise(); - responseDialog->activateWindow(); - responseDialog->setModal(true); - - // show request URL - QGroupBox *requestGroupBox = new QGroupBox("Request"); - responseLayout->addWidget(requestGroupBox); - QVBoxLayout *requestLayout = new QVBoxLayout; - requestGroupBox->setLayout(requestLayout); - QScrollArea *requestUrlScrollArea = new QScrollArea; - QLabel *requestUrlLabel = new QLabel(QString::fromStdString(response.request_url)); - requestUrlScrollArea->setWidget(requestUrlLabel); - requestLayout->addWidget(requestUrlScrollArea); - - // if there's a request body, add it to the dialog - if (response.request_body != "") { - QGroupBox *requestBodyGroupBox = new QGroupBox("Request Body"); - responseLayout->addWidget(requestBodyGroupBox); - QVBoxLayout *requestBodyLayout = new QVBoxLayout; - requestBodyGroupBox->setLayout(requestBodyLayout); - // Add scroll area for the request body - QScrollArea *requestBodyScrollArea = new QScrollArea; - QLabel *requestLabel = - new QLabel(QString::fromStdString(response.request_body)); - // Wrap the text - requestLabel->setWordWrap(true); - // Set the label as the scroll area's widget - requestBodyScrollArea->setWidget(requestLabel); - requestBodyLayout->addWidget(requestBodyScrollArea); - } + if (!response.body.empty()) { + QGroupBox *responseBodyGroupBox = new QGroupBox("Response Body"); + responseBodyGroupBox->setLayout(new QVBoxLayout); + // Add scroll area for the response body + QScrollArea *responseBodyScrollArea = new QScrollArea; + QLabel *responseLabel = + new QLabel(QString::fromStdString(response.body).trimmed()); + // Wrap the text + responseLabel->setWordWrap(true); + // dont allow rich text + responseLabel->setTextFormat(Qt::PlainText); + // Set the label as the scroll area's widget + responseBodyScrollArea->setWidget(responseLabel); + responseBodyGroupBox->layout()->addWidget(responseBodyScrollArea); + responseLayout->addWidget(responseBodyGroupBox); + } - QGroupBox *responseBodyGroupBox = new QGroupBox("Response Body"); - responseBodyGroupBox->setLayout(new QVBoxLayout); - // Add scroll area for the response body - QScrollArea *responseBodyScrollArea = new QScrollArea; - QLabel *responseLabel = new QLabel(QString::fromStdString(response.body).trimmed()); - // Wrap the text - responseLabel->setWordWrap(true); - // dont allow rich text - responseLabel->setTextFormat(Qt::PlainText); - // Set the label as the scroll area's widget - responseBodyScrollArea->setWidget(responseLabel); - responseBodyGroupBox->layout()->addWidget(responseBodyScrollArea); - responseLayout->addWidget(responseBodyGroupBox); - - // If there's a parsed output, add it to the dialog in a QGroupBox - if (response.body_parts_parsed.size() > 0 && response.body_parts_parsed[0] != "") { - QGroupBox *parsedOutputGroupBox = new QGroupBox("Parsed Output"); - responseLayout->addWidget(parsedOutputGroupBox); - QVBoxLayout *parsedOutputLayout = new QVBoxLayout; - parsedOutputGroupBox->setLayout(parsedOutputLayout); - if (response.body_parts_parsed.size() > 1) { - // Add a QTabWidget to show the parsed output parts - QTabWidget *tabWidget = new QTabWidget; - parsedOutputLayout->addWidget(tabWidget); - for (auto &parsedOutput : response.body_parts_parsed) { - // label each tab {outputN} where N is the index of the output part - tabWidget->addTab( - new QLabel(QString::fromStdString(parsedOutput)), - QString::fromStdString( - "{output" + - std::to_string(tabWidget->count()) + "}")); + // If there's a parsed output, add it to the dialog in a QGroupBox + if (response.body_parts_parsed.size() > 0 && + response.body_parts_parsed[0] != "") { + QGroupBox *parsedOutputGroupBox = new QGroupBox("Parsed Output"); + responseLayout->addWidget(parsedOutputGroupBox); + QVBoxLayout *parsedOutputLayout = new QVBoxLayout; + parsedOutputGroupBox->setLayout(parsedOutputLayout); + if (response.body_parts_parsed.size() > 1) { + // Add a QTabWidget to show the parsed output parts + QTabWidget *tabWidget = new QTabWidget; + parsedOutputLayout->addWidget(tabWidget); + for (auto &parsedOutput : response.body_parts_parsed) { + // label each tab {outputN} where N is the index of the output part + tabWidget->addTab( + new QLabel(QString::fromStdString( + parsedOutput)), + QString::fromStdString( + "{output" + + std::to_string(tabWidget->count()) + + "}")); + } + } else { + // Add a QLabel to show a single parsed output + QLabel *parsedOutputLabel = + new QLabel(QString::fromStdString( + response.body_parts_parsed[0])); + parsedOutputLabel->setWordWrap(true); + parsedOutputLabel->setTextFormat(Qt::PlainText); + parsedOutputLayout->addWidget(parsedOutputLabel); } - } else { - // Add a QLabel to show a single parsed output - QLabel *parsedOutputLabel = new QLabel( - QString::fromStdString(response.body_parts_parsed[0])); - parsedOutputLabel->setWordWrap(true); - parsedOutputLabel->setTextFormat(Qt::PlainText); - parsedOutputLayout->addWidget(parsedOutputLabel); } - } - // Resize the dialog to fit the text - responseDialog->adjustSize(); + // enable the send button + this->ui->sendButton->setEnabled(true); + + // Resize the dialog to fit the text + responseDialog->adjustSize(); + }; + + request_data_handler_ex(request_data_test); + + // TODO: Create a thread to send the request to prevent the UI from hanging }); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, [=]() { @@ -429,4 +476,6 @@ RequestBuilder::RequestBuilder(url_source_request_data *request_data, // Close dialog close(); }); + + adjustSize(); } diff --git a/src/ui/requestbuilder.ui b/src/ui/requestbuilder.ui index 923f502..032066d 100644 --- a/src/ui/requestbuilder.ui +++ b/src/ui/requestbuilder.ui @@ -2,20 +2,6 @@ RequestBuilder - - - 0 - 0 - 511 - 855 - - - - - 0 - 0 - - Dialog @@ -141,6 +127,12 @@ + + + 0 + 0 + + URL Request Options @@ -294,28 +286,38 @@ - Skip empty + No empty - Skip same + No same + + + + + + + Aggregate the input until empty is found, then use the aggregate value for the request. + + + Agg to empty - + - + @@ -393,7 +395,7 @@ - -1 + 7 0 @@ -441,24 +443,21 @@ - + Body - + - + 0 - 0 + 1 - - QAbstractScrollArea::AdjustToContents - false @@ -467,7 +466,7 @@ - + SSL Options @@ -479,6 +478,12 @@ + + + 0 + 0 + + Output Parsing Options @@ -489,6 +494,13 @@ 3 + + + + Content-Type + + + @@ -502,6 +514,16 @@ Text + + + Image (data) + + + + + Audio (data) + + JSON @@ -524,13 +546,6 @@ - - - - Content-Type - - - @@ -609,6 +624,16 @@ + + + + CSS Selector + + + + + + @@ -647,21 +672,17 @@ - - - - CSS Selector - - - - - - + + + 0 + 0 + + QLabel { color : red; } @@ -672,6 +693,12 @@ + + + 0 + 0 + + 0 diff --git a/src/url-source.cpp b/src/url-source.cpp index 2730b7f..039c2eb 100644 --- a/src/url-source.cpp +++ b/src/url-source.cpp @@ -47,9 +47,9 @@ struct url_source_data { bool send_to_stream = false; // Text source to output the text to - obs_weak_source_t *text_source = nullptr; - char *text_source_name = nullptr; - std::mutex *text_source_mutex = nullptr; + obs_weak_source_t *output_source = nullptr; + char *output_source_name = nullptr; + std::mutex *output_source_mutex = nullptr; // Use std for thread and mutex std::mutex *curl_mutex = nullptr; @@ -58,6 +58,12 @@ struct url_source_data { bool curl_thread_run = false; }; +inline bool is_valid_output_source_name(const char *output_source_name) +{ + return output_source_name != nullptr && strcmp(output_source_name, "none") != 0 && + strcmp(output_source_name, "(null)") != 0 && strcmp(output_source_name, "") != 0; +} + inline uint64_t get_time_ns(void) { return std::chrono::duration_cast( @@ -93,19 +99,19 @@ void url_source_destroy(void *data) stop_and_join_curl_thread(usd); - if (usd->text_source_name) { - bfree(usd->text_source_name); - usd->text_source_name = nullptr; + if (usd->output_source_name) { + bfree(usd->output_source_name); + usd->output_source_name = nullptr; } - if (usd->text_source) { - obs_weak_source_release(usd->text_source); - usd->text_source = nullptr; + if (usd->output_source) { + obs_weak_source_release(usd->output_source); + usd->output_source = nullptr; } - if (usd->text_source_mutex) { - delete usd->text_source_mutex; - usd->text_source_mutex = nullptr; + if (usd->output_source_mutex) { + delete usd->output_source_mutex; + usd->output_source_mutex = nullptr; } if (usd->curl_mutex) { @@ -126,56 +132,50 @@ void url_source_destroy(void *data) bfree(usd); } -void acquire_weak_text_source_ref(struct url_source_data *usd) +void acquire_weak_output_source_ref(struct url_source_data *usd) { - if (!usd->text_source_name) { - obs_log(LOG_ERROR, "text_source_name is null"); - return; - } - - if (strcmp(usd->text_source_name, "none") == 0 || - strcmp(usd->text_source_name, "(null)") == 0 || - strcmp(usd->text_source_name, "") == 0) { + if (!is_valid_output_source_name(usd->output_source_name)) { + obs_log(LOG_ERROR, "output_source_name is invalid"); // text source is not selected return; } - if (!usd->text_source_mutex) { - obs_log(LOG_ERROR, "text_source_mutex is null"); + if (!usd->output_source_mutex) { + obs_log(LOG_ERROR, "output_source_mutex is null"); return; } - std::lock_guard lock(*usd->text_source_mutex); + std::lock_guard lock(*usd->output_source_mutex); // acquire a weak ref to the new text source - obs_source_t *source = obs_get_source_by_name(usd->text_source_name); + obs_source_t *source = obs_get_source_by_name(usd->output_source_name); if (source) { - usd->text_source = obs_source_get_weak_source(source); + usd->output_source = obs_source_get_weak_source(source); obs_source_release(source); - if (!usd->text_source) { + if (!usd->output_source) { obs_log(LOG_ERROR, "failed to get weak source for text source %s", - usd->text_source_name); + usd->output_source_name); } } else { - obs_log(LOG_ERROR, "text source '%s' not found", usd->text_source_name); + obs_log(LOG_ERROR, "text source '%s' not found", usd->output_source_name); } } void setTextCallback(const std::string &str, struct url_source_data *usd) { - if (!usd->text_source_mutex) { - obs_log(LOG_ERROR, "text_source_mutex is null"); + if (!usd->output_source_mutex) { + obs_log(LOG_ERROR, "output_source_mutex is null"); return; } - if (!usd->text_source) { + if (!usd->output_source) { // attempt to acquire a weak ref to the text source if it's yet available - acquire_weak_text_source_ref(usd); + acquire_weak_output_source_ref(usd); } - std::lock_guard lock(*usd->text_source_mutex); + std::lock_guard lock(*usd->output_source_mutex); - obs_weak_source_t *text_source = usd->text_source; + obs_weak_source_t *text_source = usd->output_source; if (!text_source) { obs_log(LOG_ERROR, "text_source is null"); return; @@ -191,6 +191,42 @@ void setTextCallback(const std::string &str, struct url_source_data *usd) obs_source_release(target); }; +void setAudioCallback(const std::string &str, struct url_source_data *usd) +{ + if (!usd->output_source_mutex) { + obs_log(LOG_ERROR, "output_source_mutex is null"); + return; + } + + if (!usd->output_source) { + // attempt to acquire a weak ref to the output source if it's yet available + acquire_weak_output_source_ref(usd); + } + + std::lock_guard lock(*usd->output_source_mutex); + + obs_weak_source_t *media_source = usd->output_source; + if (!media_source) { + obs_log(LOG_ERROR, "output_source is null"); + return; + } + auto target = obs_weak_source_get_source(media_source); + if (!target) { + obs_log(LOG_ERROR, "output_source target is null"); + return; + } + // assert the source is a media source + if (strcmp(obs_source_get_id(target), "ffmpeg_source") != 0) { + obs_log(LOG_ERROR, "output_source is not a media source"); + return; + } + auto media_settings = obs_source_get_settings(target); + obs_log(LOG_INFO, "Setting media source %s to %s", usd->output_source_name, str.c_str()); + obs_data_set_string(media_settings, "file", str.c_str()); + obs_source_update(target, media_settings); + obs_source_release(target); +}; + void curl_loop(struct url_source_data *usd) { obs_log(LOG_INFO, "Starting URL Source thread, update timer: %d", usd->update_timer_ms); @@ -199,6 +235,8 @@ void curl_loop(struct url_source_data *usd) usd->frame.format = VIDEO_FORMAT_BGRA; + inja::Environment env; + while (true) { { std::lock_guard lock(*(usd->curl_mutex)); @@ -229,94 +267,116 @@ void curl_loop(struct url_source_data *usd) uint32_t width = 0; uint32_t height = 0; - // prepare the text from the template - std::string text = usd->output_text_template; - // if the template is empty use the response body - if (text.empty()) { - text = response.body_parts_parsed[0]; - } else { - // if output is image URL - fetch the image and convert it to base64 - if (usd->output_is_image_url) { - // use fetch_image to get the image - std::vector image_data = - fetch_image(response.body_parts_parsed[0]); - // convert the image to base64 - const std::string base64_image = base64_encode(image_data); - // build an image tag with the base64 image - response.body_parts_parsed[0] = - ""; + if (usd->request_data.output_type == "Audio (data)") { + if (!is_valid_output_source_name(usd->output_source_name)) { + obs_log(LOG_ERROR, + "Must select an output source for audio output"); + } else { + setAudioCallback(response.body, usd); } - try { - // Use Inja to render the template - inja::Environment env; - nlohmann::json data; - if (response.body_parts_parsed.size() > 1) { - for (size_t i = 0; - i < response.body_parts_parsed.size(); i++) { - data["output" + std::to_string(i)] = - response.body_parts_parsed[i]; + } else { + // prepare the text from the template + std::string text = usd->output_text_template; + // if the template is empty use the response body + if (text.empty()) { + text = response.body_parts_parsed[0]; + } else { + // if output is image URL - fetch the image and convert it to base64 + if (usd->output_is_image_url || + usd->request_data.output_type == "Image (data)") { + // use fetch_image to get the image + std::vector image_data; + if (usd->request_data.output_type == + "Image (data)") { + // if the output type is image data - use the response body bytes + image_data = response.body_bytes; + } else { + fetch_image(response.body_parts_parsed[0]); + } + // convert the image to base64 + const std::string base64_image = + base64_encode(image_data); + // get the mime type from the response headers if available + std::string mime_type = "image/png"; + if (response.headers.find("content-type") != + response.headers.end()) { + mime_type = + response.headers["content-type"]; + } + // build an image tag with the base64 image + response.body_parts_parsed[0] = + ""; + } + try { + // Use Inja to render the template + nlohmann::json data; + if (response.body_parts_parsed.size() > 1) { + for (size_t i = 0; + i < response.body_parts_parsed.size(); + i++) { + data["output" + std::to_string(i)] = + response.body_parts_parsed[i]; + } + // in "output" add an array of all the outputs + data["output"] = response.body_parts_parsed; + } else { + data["output"] = + response.body_parts_parsed[0]; } - // in "output" add an array of all the outputs - data["output"] = response.body_parts_parsed; - } else { - data["output"] = response.body_parts_parsed[0]; + data["body"] = response.body_json; + text = env.render(text, data); + } catch (std::exception &e) { + obs_log(LOG_ERROR, "Failed to parse template: %s", + e.what()); } - data["body"] = response.body_json; - text = env.render(text, data); - } catch (std::exception &e) { - obs_log(LOG_ERROR, "Failed to parse template: %s", - e.what()); } - } - if (usd->send_to_stream && !usd->output_is_image_url) { - // Send the output to the current stream as caption, if it's not an image and a stream is open - obs_output_t *streaming_output = - obs_frontend_get_streaming_output(); - if (streaming_output) { - obs_output_output_caption_text1(streaming_output, - text.c_str()); - obs_output_release(streaming_output); + if (usd->send_to_stream && !usd->output_is_image_url) { + // Send the output to the current stream as caption, if it's not an image and a stream is open + obs_output_t *streaming_output = + obs_frontend_get_streaming_output(); + if (streaming_output) { + obs_output_output_caption_text1(streaming_output, + text.c_str()); + obs_output_release(streaming_output); + } } - } - if (usd->frame.data[0] != nullptr) { - // Free the old render buffer - bfree(usd->frame.data[0]); - usd->frame.data[0] = nullptr; - } + if (usd->frame.data[0] != nullptr) { + // Free the old render buffer + bfree(usd->frame.data[0]); + usd->frame.data[0] = nullptr; + } - if (usd->text_source_name != nullptr && - strcmp(usd->text_source_name, "none") != 0 && - strcmp(usd->text_source_name, "(null)") != 0 && - strcmp(usd->text_source_name, "") != 0) { - // If a text source is selected - use it for rendering - setTextCallback(text, usd); - - // Update the frame with an empty buffer of 1x1 pixels - usd->frame.data[0] = (uint8_t *)bzalloc(4); - usd->frame.linesize[0] = 4; - usd->frame.width = 1; - usd->frame.height = 1; - - // Send the frame - obs_source_output_video(usd->source, &usd->frame); - } else { - uint8_t *renderBuffer = nullptr; - // render the text with QTextDocument - render_text_with_qtextdocument(text, width, height, &renderBuffer, - usd->css_props); - // Update the frame - usd->frame.data[0] = renderBuffer; - usd->frame.linesize[0] = width * 4; - usd->frame.width = width; - usd->frame.height = height; - - // Send the frame - obs_source_output_video(usd->source, &usd->frame); - } - } + if (is_valid_output_source_name(usd->output_source_name)) { + // If an output source is selected - use it for rendering + setTextCallback(text, usd); + + // Update the frame with an empty buffer of 1x1 pixels + usd->frame.data[0] = (uint8_t *)bzalloc(4); + usd->frame.linesize[0] = 4; + usd->frame.width = 1; + usd->frame.height = 1; + + // Send the frame + obs_source_output_video(usd->source, &usd->frame); + } else { + uint8_t *renderBuffer = nullptr; + // render the text with QTextDocument + render_text_with_qtextdocument( + text, width, height, &renderBuffer, usd->css_props); + // Update the frame + usd->frame.data[0] = renderBuffer; + usd->frame.linesize[0] = width * 4; + usd->frame.width = width; + usd->frame.height = height; + + // Send the frame + obs_source_output_video(usd->source, &usd->frame); + } // end if not text source + } // end if not audio + } // end if request success // time the request, calculate the remaining time and sleep const uint64_t request_end_time_ns = get_time_ns(); @@ -377,11 +437,11 @@ void *url_source_create(obs_data_t *settings, obs_source_t *source) usd->send_to_stream = obs_data_get_bool(settings, "send_to_stream"); // initialize the mutex - usd->text_source_mutex = new std::mutex(); + usd->output_source_mutex = new std::mutex(); usd->curl_mutex = new std::mutex(); usd->curl_thread_cv = new std::condition_variable(); - usd->text_source_name = bstrdup(obs_data_get_string(settings, "text_sources")); - usd->text_source = nullptr; + usd->output_source_name = bstrdup(obs_data_get_string(settings, "text_sources")); + usd->output_source = nullptr; if (obs_source_active(source) && obs_source_showing(source)) { // start the thread @@ -411,29 +471,28 @@ void url_source_update(void *data, obs_data_t *settings) const char *new_text_source_name = obs_data_get_string(settings, "text_sources"); obs_weak_source_t *old_weak_text_source = NULL; - if (strcmp(new_text_source_name, "none") == 0 || - strcmp(new_text_source_name, "(null)") == 0) { + if (!is_valid_output_source_name(new_text_source_name)) { // new selected text source is not valid, release the old one - if (usd->text_source) { - std::lock_guard lock(*usd->text_source_mutex); - old_weak_text_source = usd->text_source; - usd->text_source = nullptr; + if (usd->output_source) { + std::lock_guard lock(*usd->output_source_mutex); + old_weak_text_source = usd->output_source; + usd->output_source = nullptr; } - if (usd->text_source_name) { - bfree(usd->text_source_name); - usd->text_source_name = nullptr; + if (usd->output_source_name) { + bfree(usd->output_source_name); + usd->output_source_name = nullptr; } } else { // new selected text source is valid, check if it's different from the old one - if (usd->text_source_name == nullptr || - strcmp(new_text_source_name, usd->text_source_name) != 0) { + if (usd->output_source_name == nullptr || + strcmp(new_text_source_name, usd->output_source_name) != 0) { // new text source is different from the old one, release the old one - if (usd->text_source) { - std::lock_guard lock(*usd->text_source_mutex); - old_weak_text_source = usd->text_source; - usd->text_source = nullptr; + if (usd->output_source) { + std::lock_guard lock(*usd->output_source_mutex); + old_weak_text_source = usd->output_source; + usd->output_source = nullptr; } - usd->text_source_name = bstrdup(new_text_source_name); + usd->output_source_name = bstrdup(new_text_source_name); } } @@ -499,15 +558,26 @@ bool setup_request_button_click(obs_properties_t *, obs_property_t *, void *butt bool add_sources_to_list(void *list_property, obs_source_t *source) { + // add all text and media sources to the list auto source_id = obs_source_get_id(source); if (strcmp(source_id, "text_ft2_source_v2") != 0 && - strcmp(source_id, "text_gdiplus_v2") != 0) { + strcmp(source_id, "text_gdiplus_v2") != 0 && strcmp(source_id, "ffmpeg_source") != 0) { return true; } obs_property_t *sources = (obs_property_t *)list_property; const char *name = obs_source_get_name(source); - obs_property_list_add_string(sources, name, name); + std::string name_with_prefix; + // add a prefix to the name to indicate the source type + if (strcmp(source_id, "text_ft2_source_v2") == 0 || + strcmp(source_id, "text_gdiplus_v2") == 0) { + name_with_prefix = std::string("(Text) ").append(name); + } else if (strcmp(source_id, "image_source") == 0) { + name_with_prefix = std::string("(Image) ").append(name); + } else if (strcmp(source_id, "ffmpeg_source") == 0) { + name_with_prefix = std::string("(Media) ").append(name); + } + obs_property_list_add_string(sources, name_with_prefix.c_str(), name); return true; }