├── .github ├── FUNDING.yml ├── scripts │ ├── utils.zsh │ │ ├── log_output │ │ ├── log_info │ │ ├── log_status │ │ ├── log_warning │ │ ├── log_error │ │ ├── mkcd │ │ ├── log_debug │ │ ├── log_group │ │ ├── check_macos │ │ ├── set_loglevel │ │ ├── setup_ubuntu │ │ └── check_ubuntu │ ├── .Brewfile │ ├── .Aptfile │ ├── utils.pwsh │ │ ├── Ensure-Location.ps1 │ │ ├── Invoke-External.ps1 │ │ ├── Install-BuildDependencies.ps1 │ │ ├── Expand-ArchiveExt.ps1 │ │ └── Logger.ps1 │ ├── Package-Windows.ps1 │ ├── Build-Windows.ps1 │ ├── build-macos │ ├── package-macos │ └── package-ubuntu ├── workflows │ ├── dispatch.yaml │ ├── check-format.yaml │ ├── pr-pull.yaml │ └── push.yaml └── actions │ ├── run-gersemi │ └── action.yaml │ ├── run-clang-format │ └── action.yaml │ ├── check-changes │ └── action.yaml │ ├── build-plugin │ └── action.yaml │ └── package-plugin │ └── action.yaml ├── .gitmodules ├── cmake ├── BuildJSONCONS.cmake ├── windows │ ├── defaults.cmake │ ├── buildspec.cmake │ ├── resources │ │ ├── resource.rc.in │ │ └── installer-Windows.iss.in │ ├── compilerconfig.cmake │ └── helpers.cmake ├── BuildInja.cmake ├── macos │ ├── resources │ │ ├── ccache-launcher-c.in │ │ ├── ccache-launcher-cxx.in │ │ ├── distribution.in │ │ └── create-package.cmake.in │ ├── buildspec.cmake │ ├── defaults.cmake │ ├── compilerconfig.cmake │ └── helpers.cmake ├── linux │ ├── toolchains │ │ ├── x86_64-linux-gcc.cmake │ │ ├── aarch64-linux-gcc.cmake │ │ ├── aarch64-linux-clang.cmake │ │ └── x86_64-linux-clang.cmake │ ├── helpers.cmake │ ├── defaults.cmake │ └── compilerconfig.cmake ├── common │ ├── ccache.cmake │ ├── buildnumber.cmake │ ├── osconfig.cmake │ ├── helpers_common.cmake │ ├── compiler_common.cmake │ └── bootstrap.cmake ├── BuildPugiXML.cmake ├── BuildLexbor.cmake ├── FetchWebsocketpp.cmake └── BuildMyCurl.cmake ├── src ├── ui │ ├── obs-ui-utils.h │ ├── text-render-helper.h │ ├── CustomTextDocument.h │ ├── InputWidget.h │ ├── InputsDialog.h │ ├── RequestBuilder.h │ ├── outputmapping.h │ ├── obs-ui-utils.cpp │ ├── CustomTextDocument.cpp │ ├── CollapseButton.h │ ├── InputsDialog.cpp │ ├── text-render-helper.cpp │ ├── InputWidget.cpp │ └── inputsdialog.ui ├── parsers │ ├── errors.h │ ├── CMakeLists.txt │ ├── errors.cpp │ ├── key-value.cpp │ ├── regex.cpp │ ├── jsonpointer.cpp │ ├── parsers.h │ ├── jsonpath.cpp │ ├── xml.cpp │ ├── binary-data.cpp │ └── html.cpp ├── url-source-thread.h ├── websocket-client.h ├── url-source-callbacks.h ├── url-source-info.c ├── string-util.h ├── url-source.h ├── url-source-data.h ├── plugin-support.h ├── plugin-main.c ├── mapping-data.h ├── plugin-support.c.in ├── obs-source-util.h ├── url-source-thread.cpp ├── mapping-data.cpp ├── obs-source-util.cpp ├── websocket-client.cpp └── request-data.h ├── .gersemirc ├── data └── locale │ └── en-US.ini ├── .gitignore ├── CMakeLists.txt ├── CMakePresets.json └── .clang-format /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [royshil] 2 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/log_output: -------------------------------------------------------------------------------- 1 | print -PR " ${@}" 2 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/log_info: -------------------------------------------------------------------------------- 1 | print -PR "%F{4} =>%f %B${@}%b" 2 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/log_status: -------------------------------------------------------------------------------- 1 | print -PR "%F{2} >%f ${@}" 2 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/log_warning: -------------------------------------------------------------------------------- 1 | print -PR "::warning::%F{3} => ${@}%f" 2 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/log_error: -------------------------------------------------------------------------------- 1 | print -u2 -PR "::error::%F{1} ✖︎%f ${@}" 2 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/mkcd: -------------------------------------------------------------------------------- 1 | [[ -n ${1} ]] && mkdir -p ${1} && builtin cd ${1} 2 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/log_debug: -------------------------------------------------------------------------------- 1 | if (( debug )) print -PR -e "::debug::%F{220}DEBUG: ${@}%f" 2 | -------------------------------------------------------------------------------- /.github/scripts/.Brewfile: -------------------------------------------------------------------------------- 1 | brew "ccache" 2 | brew "coreutils" 3 | brew "cmake" 4 | brew "jq" 5 | brew "xcbeautify" 6 | -------------------------------------------------------------------------------- /.github/scripts/.Aptfile: -------------------------------------------------------------------------------- 1 | package 'cmake' 2 | package 'ccache' 3 | package 'git' 4 | package 'jq' 5 | package 'ninja-build', bin: 'ninja' 6 | package 'pkg-config' 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/jsoncons"] 2 | path = vendor/jsoncons 3 | url = https://github.com/danielaparker/jsoncons.git 4 | [submodule "vendor/inja"] 5 | path = vendor/inja 6 | url = https://github.com/pantor/inja 7 | -------------------------------------------------------------------------------- /cmake/BuildJSONCONS.cmake: -------------------------------------------------------------------------------- 1 | set(JSONCONS_BUILD_TESTS OFF) 2 | add_compile_options(-Wno-conversion -Wno-error=conversion) 3 | add_subdirectory(${CMAKE_SOURCE_DIR}/vendor/jsoncons ${CMAKE_BINARY_DIR}/jsoncons EXCLUDE_FROM_ALL) 4 | -------------------------------------------------------------------------------- /src/ui/obs-ui-utils.h: -------------------------------------------------------------------------------- 1 | #ifndef UI_OBS_UTILS_H 2 | #define UI_OBS_UTILS_H 3 | 4 | #include 5 | 6 | bool add_sources_to_combobox(void *list_property, obs_source_t *source); 7 | 8 | #endif // UI_OBS_UTILS_H 9 | -------------------------------------------------------------------------------- /cmake/windows/defaults.cmake: -------------------------------------------------------------------------------- 1 | # CMake Windows defaults module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Enable find_package targets to become globally available targets 6 | set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL TRUE) 7 | 8 | include(buildspec) 9 | -------------------------------------------------------------------------------- /src/parsers/errors.h: -------------------------------------------------------------------------------- 1 | #ifndef PARSERS_ERRORS_H 2 | #define PARSERS_ERRORS_H 3 | 4 | #include "request-data.h" 5 | 6 | struct request_data_handler_response make_fail_parse_response(const std::string &error_message); 7 | 8 | #endif // PARSERS_ERRORS_H 9 | -------------------------------------------------------------------------------- /src/url-source-thread.h: -------------------------------------------------------------------------------- 1 | #ifndef URL_SOURCE_THREAD_H 2 | #define URL_SOURCE_THREAD_H 3 | 4 | #include "url-source-data.h" 5 | 6 | void curl_loop(struct url_source_data *usd); 7 | void stop_and_join_curl_thread(struct url_source_data *usd); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /src/websocket-client.h: -------------------------------------------------------------------------------- 1 | #ifndef WEBSOCKET_CLIENT_H 2 | #define WEBSOCKET_CLIENT_H 3 | 4 | #include "request-data.h" 5 | 6 | struct request_data_handler_response 7 | websocket_request_handler(url_source_request_data *request_data); 8 | 9 | #endif // WEBSOCKET_CLIENT_H 10 | -------------------------------------------------------------------------------- /.gersemirc: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/BlankSpruce/gersemi/master/gersemi/configuration.schema.json 2 | 3 | definitions: [] 4 | line_length: 120 5 | indent: 2 6 | list_expansion: favour-inlining 7 | unsafe: false 8 | warn_about_unknown_commands: false 9 | -------------------------------------------------------------------------------- /src/ui/text-render-helper.h: -------------------------------------------------------------------------------- 1 | #ifndef TEXT_RENDER_HELPER_H 2 | #define TEXT_RENDER_HELPER_H 3 | 4 | void render_text_with_qtextdocument(const std::string &text, uint32_t &width, uint32_t &height, 5 | uint8_t **data, const std::string &css_props); 6 | 7 | #endif // TEXT_RENDER_HELPER_H 8 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/log_group: -------------------------------------------------------------------------------- 1 | autoload -Uz log_info 2 | 3 | if (( ! ${+_log_group} )) typeset -g _log_group=0 4 | 5 | if (( _log_group )) { 6 | print "::endgroup::" 7 | typeset -g _log_group=0 8 | } 9 | if (( # )) { 10 | print "::group::${@}" 11 | typeset -g _log_group=1 12 | } 13 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/check_macos: -------------------------------------------------------------------------------- 1 | autoload -Uz is-at-least log_group log_info log_error log_status 2 | 3 | log_info 'Checking for Homebrew...' 4 | if (( ! ${+commands[brew]} )) { 5 | log_error 'No Homebrew command found. Please install Homebrew (https://brew.sh)' 6 | return 2 7 | } 8 | 9 | brew bundle --file ${SCRIPT_HOME}/.Brewfile 10 | rehash 11 | log_group 12 | -------------------------------------------------------------------------------- /data/locale/en-US.ini: -------------------------------------------------------------------------------- 1 | url_file="URL / File" 2 | setup_data_source="Setup Data Source" 3 | setup_outputs_and_templates="Setup Outputs and Templates" 4 | update_timer_ms="Update Timer (ms)" 5 | run_while_not_visible="Run while not visible?" 6 | send_output_to_stream="Send output to current stream as captions" 7 | output_is_image_url="Output is image URL (fetch and show image)" 8 | render_width="Render Width (px)" 9 | -------------------------------------------------------------------------------- /src/url-source-callbacks.h: -------------------------------------------------------------------------------- 1 | #ifndef URL_SOURCE_CALLBACKS_H 2 | #define URL_SOURCE_CALLBACKS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "request-data.h" 11 | 12 | void output_with_mapping(const request_data_handler_response &response, 13 | struct url_source_data *usd); 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /cmake/BuildInja.cmake: -------------------------------------------------------------------------------- 1 | set(INJA_USE_EMBEDDED_JSON UNIX AND NOT APPLE) 2 | set(INJA_INSTALL OFF) 3 | set(INJA_EXPORT OFF) 4 | set(BUILD_TESTING OFF) 5 | set(INJA_BUILD_TESTS OFF) 6 | set(BUILD_BENCHMARK OFF) 7 | set(COVERALLS OFF) 8 | # inja uses a lot of shadowing 9 | add_compile_options(-Wno-shadow -Wno-error=shadow -Wno-shadow-field -Wno-error=shadow-field) 10 | add_subdirectory(${CMAKE_SOURCE_DIR}/vendor/inja ${CMAKE_BINARY_DIR}/inja EXCLUDE_FROM_ALL) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Exclude everything 2 | /* 3 | 4 | # Except for default project files 5 | !/.github 6 | !/build-aux 7 | !/cmake 8 | !/data 9 | !/src 10 | !/vendor 11 | !.clang-format 12 | !.gersemirc 13 | !.gitignore 14 | !buildspec.json 15 | !CMakeLists.txt 16 | !CMakePresets.json 17 | !LICENSE 18 | !README.md 19 | 20 | # Exclude lock files 21 | *.lock.json 22 | 23 | # Exclude macOS legacy resource forks 24 | .DS_Store 25 | 26 | # Exclude CMake build number cache 27 | /cmake/.CMakeBuildNumber 28 | -------------------------------------------------------------------------------- /src/parsers/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | target_sources( 2 | ${CMAKE_PROJECT_NAME} 3 | PRIVATE binary-data.cpp 4 | errors.cpp 5 | html.cpp 6 | jsonpath.cpp 7 | jsonpointer.cpp 8 | key-value.cpp 9 | regex.cpp 10 | xml.cpp) 11 | 12 | # on linux, disable conversion errors 13 | if(UNIX AND NOT APPLE) 14 | set(CMAKE_COMPILE_WARNING_AS_ERROR OFF) 15 | add_compile_options(-Wno-error -Wno-conversion -Wno-shadow -Wno-unused-parameter) 16 | endif() 17 | -------------------------------------------------------------------------------- /.github/workflows/dispatch.yaml: -------------------------------------------------------------------------------- 1 | name: Dispatch 2 | run-name: Dispatched Repository Actions - ${{ inputs.job }} ⌛️ 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | job: 7 | description: Dispatch job to run 8 | required: true 9 | type: choice 10 | options: 11 | - build 12 | permissions: 13 | contents: write 14 | jobs: 15 | check-and-build: 16 | if: inputs.job == 'build' 17 | uses: ./.github/workflows/build-project.yaml 18 | secrets: inherit 19 | -------------------------------------------------------------------------------- /src/url-source-info.c: -------------------------------------------------------------------------------- 1 | #include "url-source.h" 2 | 3 | struct obs_source_info url_source = { 4 | .id = "url_source", 5 | .type = OBS_SOURCE_TYPE_INPUT, 6 | .output_flags = OBS_SOURCE_ASYNC_VIDEO, 7 | .get_name = url_source_name, 8 | .create = url_source_create, 9 | .destroy = url_source_destroy, 10 | .get_defaults = url_source_defaults, 11 | .get_properties = url_source_properties, 12 | .update = url_source_update, 13 | .activate = url_source_activate, 14 | .deactivate = url_source_deactivate, 15 | }; 16 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/set_loglevel: -------------------------------------------------------------------------------- 1 | autoload -Uz log_debug log_error 2 | 3 | local -r _usage="Usage: %B${0}%b 4 | 5 | Set log level, following levels are supported: 0 (quiet), 1 (normal), 2 (verbose), 3 (debug)" 6 | 7 | if (( ! # )); then 8 | log_error 'Called without arguments.' 9 | log_output ${_usage} 10 | return 2 11 | elif (( ${1} >= 4 )); then 12 | log_error 'Called with loglevel > 3.' 13 | log_output ${_usage} 14 | fi 15 | 16 | typeset -g -i -r _loglevel=${1} 17 | log_debug "Log level set to '${1}'" 18 | -------------------------------------------------------------------------------- /src/parsers/errors.cpp: -------------------------------------------------------------------------------- 1 | #include "plugin-support.h" 2 | #include "errors.h" 3 | 4 | #include 5 | 6 | struct request_data_handler_response make_fail_parse_response(const std::string &error_message) 7 | { 8 | obs_log(LOG_INFO, "Failed to parse response: %s", error_message.c_str()); 9 | // Build an error response 10 | struct request_data_handler_response responseFail; 11 | responseFail.error_message = error_message; 12 | responseFail.status_code = URL_SOURCE_REQUEST_PARSING_ERROR_CODE; 13 | return responseFail; 14 | } 15 | -------------------------------------------------------------------------------- /cmake/macos/resources/ccache-launcher-c.in: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [[ "$1" == "${CMAKE_C_COMPILER}" ]] ; then 4 | shift 5 | fi 6 | 7 | export CCACHE_CPP2=true 8 | export CCACHE_DEPEND=true 9 | export CCACHE_DIRECT=true 10 | export CCACHE_FILECLONE=true 11 | export CCACHE_INODECACHE=true 12 | export CCACHE_NOCOMPILERCHECK='content' 13 | export CCACHE_SLOPPINESS='include_file_mtime,include_file_ctime,clang_index_store,system_headers' 14 | if [[ "${CI}" ]]; then 15 | export CCACHE_NOHASHDIR=true 16 | fi 17 | exec "${CMAKE_C_COMPILER_LAUNCHER}" "${CMAKE_C_COMPILER}" "$@" 18 | -------------------------------------------------------------------------------- /cmake/macos/resources/ccache-launcher-cxx.in: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [[ "$1" == "${CMAKE_CXX_COMPILER}" ]] ; then 4 | shift 5 | fi 6 | 7 | export CCACHE_CPP2=true 8 | export CCACHE_NODEPEND=true 9 | export CCACHE_DIRECT=true 10 | export CCACHE_FILECLONE=true 11 | export CCACHE_INODECACHE=true 12 | export CCACHE_NOCOMPILERCHECK='content' 13 | export CCACHE_SLOPPINESS='include_file_mtime,include_file_ctime,clang_index_store,system_headers' 14 | if [[ "${CI}" ]]; then 15 | export CCACHE_NOHASHDIR=true 16 | fi 17 | exec "${CMAKE_CXX_COMPILER_LAUNCHER}" "${CMAKE_CXX_COMPILER}" "$@" 18 | -------------------------------------------------------------------------------- /src/ui/CustomTextDocument.h: -------------------------------------------------------------------------------- 1 | #ifndef CUSTOMTEXTDOCUMENT_H 2 | #define CUSTOMTEXTDOCUMENT_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class CustomTextDocument : public QTextDocument { 10 | Q_OBJECT 11 | 12 | public: 13 | CustomTextDocument(QObject *parent = nullptr); 14 | 15 | protected: 16 | QVariant loadResource(int type, const QUrl &name) override; 17 | 18 | private: 19 | static QHash imageCache; // Static member for the cache 20 | }; 21 | 22 | #endif // CUSTOMTEXTDOCUMENT_H 23 | -------------------------------------------------------------------------------- /src/ui/InputWidget.h: -------------------------------------------------------------------------------- 1 | #ifndef INPUT_WIDGET_H 2 | #define INPUT_WIDGET_H 3 | 4 | #include 5 | 6 | #include "mapping-data.h" 7 | 8 | namespace Ui { 9 | class InputWidget; 10 | } 11 | 12 | class InputWidget : public QWidget { 13 | Q_OBJECT 14 | 15 | public: 16 | explicit InputWidget(QWidget *parent = nullptr); 17 | ~InputWidget(); 18 | 19 | InputWidget(const InputWidget &) = delete; 20 | InputWidget &operator=(const InputWidget &) = delete; 21 | 22 | void setInputData(const input_data &data); 23 | input_data getInputDataFromUI(); 24 | 25 | private: 26 | Ui::InputWidget *ui; 27 | }; 28 | 29 | #endif // INPUT_WIDGET_H 30 | -------------------------------------------------------------------------------- /cmake/linux/toolchains/x86_64-linux-gcc.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_SYSTEM_NAME Linux) 2 | set(CMAKE_SYSTEM_PROCESSOR x86_64) 3 | set(CMAKE_CROSSCOMPILING TRUE) 4 | 5 | set(CMAKE_C_COMPILER /usr/bin/x86_64-linux-gnu-gcc) 6 | set(CMAKE_CXX_COMPILER /usr/bin/x86_64-linux-gnu-g++) 7 | 8 | set(CMAKE_FIND_ROOT_PATH /usr/x86_64-linux-gnu) 9 | 10 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 11 | 12 | set(PKG_CONFIG_EXECUTABLE /usr/bin/x86_64-linux-gnu-pkg-config CACHE FILEPATH "pkg-config executable") 13 | 14 | set(CPACK_READELF_EXECUTABLE /usr/bin/x86_64-linux-gnu-readelf) 15 | set(CPACK_OBJCOPY_EXECUTABLE /usr/bin/x86_64-linux-gnu-objcopy) 16 | set(CPACK_OBJDUMP_EXECUTABLE /usr/bin/x86_64-linux-gnu-objdump) 17 | set(CPACK_PACKAGE_ARCHITECTURE x86_64) 18 | set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE x86_64) 19 | -------------------------------------------------------------------------------- /cmake/linux/toolchains/aarch64-linux-gcc.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_SYSTEM_NAME Linux) 2 | set(CMAKE_SYSTEM_PROCESSOR aarch64) 3 | set(CMAKE_CROSSCOMPILING TRUE) 4 | 5 | set(CMAKE_C_COMPILER /usr/bin/aarch64-linux-gnu-gcc) 6 | set(CMAKE_CXX_COMPILER /usr/bin/aarch64-linux-gnu-g++) 7 | 8 | set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu) 9 | 10 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 11 | 12 | set(PKG_CONFIG_EXECUTABLE /usr/bin/aarch64-linux-gnu-pkg-config CACHE FILEPATH "pkg-config executable") 13 | 14 | set(CPACK_READELF_EXECUTABLE /usr/bin/aarch64-linux-gnu-readelf) 15 | set(CPACK_OBJCOPY_EXECUTABLE /usr/bin/aarch64-linux-gnu-objcopy) 16 | set(CPACK_OBJDUMP_EXECUTABLE /usr/bin/aarch64-linux-gnu-objdump) 17 | set(CPACK_PACKAGE_ARCHITECTURE arm64) 18 | set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE arm64) 19 | -------------------------------------------------------------------------------- /cmake/common/ccache.cmake: -------------------------------------------------------------------------------- 1 | # CMake ccache module 2 | 3 | include_guard(GLOBAL) 4 | 5 | if(NOT DEFINED CCACHE_PROGRAM) 6 | message(DEBUG "Trying to find ccache on build host...") 7 | find_program(CCACHE_PROGRAM "ccache") 8 | mark_as_advanced(CCACHE_PROGRAM) 9 | endif() 10 | 11 | if(CCACHE_PROGRAM) 12 | message(DEBUG "Ccache found as ${CCACHE_PROGRAM}...") 13 | option(ENABLE_CCACHE "Enable compiler acceleration with ccache" ON) 14 | 15 | if(ENABLE_CCACHE) 16 | set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") 17 | set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") 18 | set(CMAKE_OBJC_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") 19 | set(CMAKE_OBJCXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") 20 | set(CMAKE_CUDA_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") 21 | endif() 22 | endif() 23 | -------------------------------------------------------------------------------- /.github/workflows/check-format.yaml: -------------------------------------------------------------------------------- 1 | name: Check Code Formatting 🛠️ 2 | on: 3 | workflow_call: 4 | jobs: 5 | clang-format: 6 | runs-on: ubuntu-24.04 7 | permissions: 8 | contents: read 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | - name: clang-format check 🐉 14 | id: clang-format 15 | uses: ./.github/actions/run-clang-format 16 | with: 17 | failCondition: error 18 | 19 | gersemi: 20 | runs-on: ubuntu-24.04 21 | permissions: 22 | contents: read 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - name: gersemi Check 🎛️ 28 | id: gersemi 29 | uses: ./.github/actions/run-gersemi 30 | with: 31 | failCondition: error -------------------------------------------------------------------------------- /.github/workflows/pr-pull.yaml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | run-name: ${{ github.event.pull_request.title }} pull request run 🚀 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | paths-ignore: 7 | - '**.md' 8 | branches: [master, main] 9 | types: [ opened, synchronize, reopened ] 10 | permissions: 11 | contents: read 12 | concurrency: 13 | group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' 14 | cancel-in-progress: true 15 | jobs: 16 | check-format: 17 | name: Check Formatting 🔍 18 | uses: ./.github/workflows/check-format.yaml 19 | permissions: 20 | contents: read 21 | 22 | build-project: 23 | name: Build Project 🧱 24 | uses: ./.github/workflows/build-project.yaml 25 | secrets: inherit 26 | permissions: 27 | contents: read 28 | -------------------------------------------------------------------------------- /src/ui/InputsDialog.h: -------------------------------------------------------------------------------- 1 | #ifndef INPUTSDIALOG_H 2 | #define INPUTSDIALOG_H 3 | 4 | #include 5 | #include 6 | 7 | #include "mapping-data.h" 8 | 9 | namespace Ui { 10 | class InputsDialog; 11 | } 12 | 13 | class InputsDialog : public QDialog { 14 | Q_OBJECT 15 | 16 | public: 17 | explicit InputsDialog(QWidget *parent = nullptr); 18 | ~InputsDialog(); 19 | 20 | InputsDialog(const InputsDialog &) = delete; 21 | InputsDialog &operator=(const InputsDialog &) = delete; 22 | 23 | inputs_data getInputsDataFromUI(); 24 | 25 | // populate the listWidget with the inputs_data 26 | void setInputsData(const inputs_data &data); 27 | 28 | private: 29 | Ui::InputsDialog *ui; 30 | 31 | public slots: 32 | void addInput(); 33 | void removeInput(); 34 | }; 35 | 36 | #endif // INPUTSDIALOG_H 37 | -------------------------------------------------------------------------------- /src/ui/RequestBuilder.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "request-data.h" 4 | #include "mapping-data.h" 5 | 6 | namespace Ui { 7 | class RequestBuilder; 8 | } 9 | 10 | class RequestBuilder : public QDialog { 11 | Q_OBJECT 12 | public: 13 | RequestBuilder(url_source_request_data *request_data, 14 | // update handler lambda function 15 | std::function update_handler, QWidget *parent = nullptr); 16 | 17 | private slots: 18 | void show_response_dialog(const request_data_handler_response &response); 19 | void show_error_message(const std::string &error_message); 20 | 21 | signals: 22 | void show_response_dialog_signal(const request_data_handler_response &response); 23 | void show_error_message_signal(const std::string &error_message); 24 | 25 | private: 26 | Ui::RequestBuilder *ui; 27 | inputs_data inputs_data_; 28 | }; 29 | -------------------------------------------------------------------------------- /.github/scripts/utils.pwsh/Ensure-Location.ps1: -------------------------------------------------------------------------------- 1 | function Ensure-Location { 2 | <# 3 | .SYNOPSIS 4 | Ensures current location to be set to specified directory. 5 | .DESCRIPTION 6 | If specified directory exists, switch to it. Otherwise create it, 7 | then switch. 8 | .EXAMPLE 9 | Ensure-Location "My-Directory" 10 | Ensure-Location -Path "Path-To-My-Directory" 11 | #> 12 | 13 | param( 14 | [Parameter(Mandatory)] 15 | [string] $Path 16 | ) 17 | 18 | if ( ! ( Test-Path $Path ) ) { 19 | $_Params = @{ 20 | ItemType = "Directory" 21 | Path = ${Path} 22 | ErrorAction = "SilentlyContinue" 23 | } 24 | 25 | New-Item @_Params | Set-Location 26 | } else { 27 | Set-Location -Path ${Path} 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/string-util.h: -------------------------------------------------------------------------------- 1 | #ifndef STRING_UTIL_H 2 | #define STRING_UTIL_H 3 | 4 | #ifdef __cplusplus 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace { 12 | 13 | // Trim from start (left) 14 | static inline std::string <rim(std::string &s) 15 | { 16 | s.erase(s.begin(), std::find_if(s.begin(), s.end(), 17 | [](unsigned char ch) { return !std::isspace(ch); })); 18 | return s; 19 | } 20 | 21 | // Trim from end (right) 22 | static inline std::string &rtrim(std::string &s) 23 | { 24 | s.erase(std::find_if(s.rbegin(), s.rend(), 25 | [](unsigned char ch) { return !std::isspace(ch); }) 26 | .base(), 27 | s.end()); 28 | return s; 29 | } 30 | 31 | // Trim from both ends 32 | static inline std::string &trim(std::string &s) 33 | { 34 | return ltrim(rtrim(s)); 35 | } 36 | 37 | } // namespace 38 | 39 | #endif 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /cmake/windows/buildspec.cmake: -------------------------------------------------------------------------------- 1 | # CMake Windows build dependencies module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(buildspec_common) 6 | 7 | # _check_dependencies_windows: Set up Windows slice for _check_dependencies 8 | function(_check_dependencies_windows) 9 | set(arch ${CMAKE_GENERATOR_PLATFORM}) 10 | set(platform windows-${arch}) 11 | 12 | set(dependencies_dir "${CMAKE_CURRENT_SOURCE_DIR}/.deps") 13 | set(prebuilt_filename "windows-deps-VERSION-ARCH-REVISION.zip") 14 | set(prebuilt_destination "obs-deps-VERSION-ARCH") 15 | set(qt6_filename "windows-deps-qt6-VERSION-ARCH-REVISION.zip") 16 | set(qt6_destination "obs-deps-qt6-VERSION-ARCH") 17 | set(obs-studio_filename "VERSION.zip") 18 | set(obs-studio_destination "obs-studio-VERSION") 19 | set(dependencies_list prebuilt qt6 obs-studio) 20 | 21 | _check_dependencies() 22 | endfunction() 23 | 24 | _check_dependencies_windows() 25 | -------------------------------------------------------------------------------- /src/url-source.h: -------------------------------------------------------------------------------- 1 | #ifndef URL_SOURCE_H 2 | #define URL_SOURCE_H 3 | 4 | #include 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | obs_properties_t *url_source_properties(void *data); 11 | void url_source_defaults(obs_data_t *s); 12 | void *url_source_create(obs_data_t *settings, obs_source_t *source); 13 | void url_source_destroy(void *data); 14 | void url_source_update(void *data, obs_data_t *settings); 15 | const char *url_source_name(void *unused); 16 | void url_source_activate(void *data); 17 | void url_source_deactivate(void *data); 18 | 19 | const char *const PLUGIN_INFO_TEMPLATE = 20 | "URL/API Source (%1) by " 21 | "Locaal AI ❤️ " 22 | "Support & Follow"; 23 | 24 | #ifdef __cplusplus 25 | } 26 | #endif 27 | 28 | #endif /* URL_SOURCE_H */ 29 | -------------------------------------------------------------------------------- /cmake/common/buildnumber.cmake: -------------------------------------------------------------------------------- 1 | # CMake build number module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Define build number cache file 6 | set( 7 | _BUILD_NUMBER_CACHE 8 | "${CMAKE_CURRENT_SOURCE_DIR}/cmake/.CMakeBuildNumber" 9 | CACHE INTERNAL 10 | "OBS build number cache file" 11 | ) 12 | 13 | # Read build number from cache file or manual override 14 | if(NOT DEFINED PLUGIN_BUILD_NUMBER AND EXISTS "${_BUILD_NUMBER_CACHE}") 15 | file(READ "${_BUILD_NUMBER_CACHE}" PLUGIN_BUILD_NUMBER) 16 | math(EXPR PLUGIN_BUILD_NUMBER "${PLUGIN_BUILD_NUMBER}+1") 17 | elseif(NOT DEFINED PLUGIN_BUILD_NUMBER) 18 | if($ENV{CI}) 19 | if($ENV{GITHUB_RUN_ID}) 20 | set(PLUGIN_BUILD_NUMBER "$ENV{GITHUB_RUN_ID}") 21 | elseif($ENV{GITLAB_RUN_ID}) 22 | set(PLUGIN_BUILD_NUMBER "$ENV{GITLAB_RUN_ID}") 23 | else() 24 | set(PLUGIN_BUILD_NUMBER "1") 25 | endif() 26 | else() 27 | set(PLUGIN_BUILD_NUMBER "1") 28 | endif() 29 | endif() 30 | file(WRITE "${_BUILD_NUMBER_CACHE}" "${PLUGIN_BUILD_NUMBER}") 31 | -------------------------------------------------------------------------------- /src/url-source-data.h: -------------------------------------------------------------------------------- 1 | #ifndef URL_SOURCE_DATA_H 2 | #define URL_SOURCE_DATA_H 3 | 4 | #include "request-data.h" 5 | #include "mapping-data.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | struct url_source_data { 15 | obs_source_t *source = nullptr; 16 | struct url_source_request_data request_data; 17 | struct request_data_handler_response response; 18 | struct output_mapping_data output_mapping_data; 19 | uint32_t update_timer_ms = 1000; 20 | bool run_while_not_visible = false; 21 | bool output_is_image_url = false; 22 | struct obs_source_frame frame; 23 | bool send_to_stream = false; 24 | uint32_t render_width = 640; 25 | 26 | std::mutex output_mapping_mutex; 27 | std::mutex curl_mutex; 28 | std::thread curl_thread; 29 | std::condition_variable curl_thread_cv; 30 | std::atomic curl_thread_run = false; 31 | 32 | // ctor must initialize mutex 33 | explicit url_source_data(); 34 | }; 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /cmake/common/osconfig.cmake: -------------------------------------------------------------------------------- 1 | # CMake operating system bootstrap module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Set minimum CMake version specific to host operating system, add OS-specific module directory to default search paths, 6 | # and set helper variables for OS detection in other CMake list files. 7 | if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") 8 | set(CMAKE_C_EXTENSIONS FALSE) 9 | set(CMAKE_CXX_EXTENSIONS FALSE) 10 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/windows") 11 | set(OS_WINDOWS TRUE) 12 | elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin") 13 | set(CMAKE_C_EXTENSIONS FALSE) 14 | set(CMAKE_CXX_EXTENSIONS FALSE) 15 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos") 16 | set(OS_MACOS TRUE) 17 | elseif(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux|FreeBSD|OpenBSD") 18 | set(CMAKE_CXX_EXTENSIONS FALSE) 19 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/linux") 20 | string(TOUPPER "${CMAKE_HOST_SYSTEM_NAME}" _SYSTEM_NAME_U) 21 | set(OS_${_SYSTEM_NAME_U} TRUE) 22 | endif() 23 | -------------------------------------------------------------------------------- /src/ui/outputmapping.h: -------------------------------------------------------------------------------- 1 | #ifndef OUTPUTMAPPING_H 2 | #define OUTPUTMAPPING_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "mapping-data.h" 10 | 11 | namespace Ui { 12 | class OutputMapping; 13 | } 14 | 15 | typedef std::function update_handler_t; 16 | class OutputMapping : public QDialog { 17 | Q_OBJECT 18 | 19 | public: 20 | explicit OutputMapping(const output_mapping_data &mapping_data_in, 21 | update_handler_t update_handler, QWidget *parent = nullptr); 22 | ~OutputMapping(); 23 | 24 | OutputMapping(const OutputMapping &) = delete; 25 | OutputMapping &operator=(const OutputMapping &) = delete; 26 | 27 | private: 28 | Ui::OutputMapping *ui; 29 | QStandardItemModel model; 30 | output_mapping_data mapping_data; 31 | QComboBox *createSourcesComboBox(); 32 | update_handler_t update_handler; 33 | 34 | private slots: 35 | void addMapping(); 36 | void removeMapping(); 37 | }; 38 | 39 | #endif // OUTPUTMAPPING_H 40 | -------------------------------------------------------------------------------- /cmake/windows/resources/resource.rc.in: -------------------------------------------------------------------------------- 1 | 1 VERSIONINFO 2 | FILEVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0 3 | PRODUCTVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0 4 | FILEFLAGSMASK 0x0L 5 | #ifdef _DEBUG 6 | FILEFLAGS 0x1L 7 | #else 8 | FILEFLAGS 0x0L 9 | #endif 10 | FILEOS 0x0L 11 | FILETYPE 0x2L 12 | FILESUBTYPE 0x0L 13 | BEGIN 14 | BLOCK "StringFileInfo" 15 | BEGIN 16 | BLOCK "040904b0" 17 | BEGIN 18 | VALUE "CompanyName", "${PLUGIN_AUTHOR}" 19 | VALUE "FileDescription", "${PROJECT_NAME}" 20 | VALUE "FileVersion", "${PROJECT_VERSION}" 21 | VALUE "InternalName", "${PROJECT_NAME}" 22 | VALUE "LegalCopyright", "(C) ${CURRENT_YEAR} ${PLUGIN_AUTHOR}" 23 | VALUE "OriginalFilename", "${PROJECT_NAME}" 24 | VALUE "ProductName", "${PROJECT_NAME}" 25 | VALUE "ProductVersion", "${PROJECT_VERSION}" 26 | END 27 | END 28 | BLOCK "VarFileInfo" 29 | BEGIN 30 | VALUE "Translation", 0x409, 1200 31 | END 32 | END 33 | -------------------------------------------------------------------------------- /src/plugin-support.h: -------------------------------------------------------------------------------- 1 | /* 2 | URL Source 3 | Copyright (C) 2023 Roy Shilkrot roy.shil@gmail.com 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with this program. If not, see 17 | */ 18 | 19 | #pragma once 20 | 21 | #ifdef __cplusplus 22 | extern "C" { 23 | #endif 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | extern const char *PLUGIN_NAME; 31 | extern const char *PLUGIN_VERSION; 32 | 33 | void obs_log(int log_level, const char *format, ...); 34 | 35 | #define MT_ obs_module_text 36 | 37 | #ifdef __cplusplus 38 | } 39 | #endif 40 | -------------------------------------------------------------------------------- /src/parsers/key-value.cpp: -------------------------------------------------------------------------------- 1 | #include "errors.h" 2 | #include "request-data.h" 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | struct request_data_handler_response parse_key_value(struct request_data_handler_response response, 10 | const url_source_request_data *request_data) 11 | { 12 | 13 | UNUSED_PARAMETER(request_data); 14 | 15 | // Create a string stream from the body 16 | std::istringstream body_stream(response.body); 17 | std::string line; 18 | 19 | // Iterate through each line of the body 20 | while (std::getline(body_stream, line)) { 21 | // Skip empty lines 22 | if (line.empty()) 23 | continue; 24 | 25 | // Split the line by the delimiter 26 | // look for the first occurrence of the delimiter from the beginning of the line 27 | size_t delimiter_pos = line.find(request_data->kv_delimiter); 28 | if (delimiter_pos != std::string::npos) { 29 | std::string key = line.substr(0, delimiter_pos); 30 | std::string value = line.substr(delimiter_pos + 1); 31 | response.key_value_pairs[key] = value; 32 | response.body_parts_parsed.push_back(line); 33 | } 34 | } 35 | 36 | return response; 37 | } 38 | -------------------------------------------------------------------------------- /.github/scripts/utils.pwsh/Invoke-External.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-External { 2 | <# 3 | .SYNOPSIS 4 | Invokes a non-PowerShell command. 5 | .DESCRIPTION 6 | Runs a non-PowerShell command, and captures its return code. 7 | Throws an exception if the command returns non-zero. 8 | .EXAMPLE 9 | Invoke-External 7z x $MyArchive 10 | #> 11 | 12 | if ( $args.Count -eq 0 ) { 13 | throw 'Invoke-External called without arguments.' 14 | } 15 | 16 | if ( ! ( Test-Path function:Log-Information ) ) { 17 | . $PSScriptRoot/Logger.ps1 18 | } 19 | 20 | $Command = $args[0] 21 | $CommandArgs = @() 22 | 23 | if ( $args.Count -gt 1) { 24 | $CommandArgs = $args[1..($args.Count - 1)] 25 | } 26 | 27 | $_EAP = $ErrorActionPreference 28 | $ErrorActionPreference = "Continue" 29 | 30 | Log-Debug "Invoke-External: ${Command} ${CommandArgs}" 31 | 32 | & $command $commandArgs 33 | $Result = $LASTEXITCODE 34 | 35 | $ErrorActionPreference = $_EAP 36 | 37 | if ( $Result -ne 0 ) { 38 | throw "${Command} ${CommandArgs} exited with non-zero code ${Result}." 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/plugin-main.c: -------------------------------------------------------------------------------- 1 | /* 2 | URL Source 3 | Copyright (C) 2023 Roy Shilkrot roy.shil@gmail.com 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with this program. If not, see 17 | */ 18 | 19 | #include 20 | #include 21 | 22 | OBS_DECLARE_MODULE() 23 | OBS_MODULE_USE_DEFAULT_LOCALE(PLUGIN_NAME, "en-US") 24 | 25 | extern struct obs_source_info url_source; 26 | 27 | bool obs_module_load(void) 28 | { 29 | obs_log(LOG_INFO, "plugin loaded successfully (version %s)", PLUGIN_VERSION); 30 | obs_register_source(&url_source); 31 | return true; 32 | } 33 | 34 | void obs_module_unload(void) 35 | { 36 | obs_log(LOG_INFO, "plugin unloaded"); 37 | } 38 | -------------------------------------------------------------------------------- /cmake/macos/buildspec.cmake: -------------------------------------------------------------------------------- 1 | # CMake macOS build dependencies module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(buildspec_common) 6 | 7 | # _check_dependencies_macos: Set up macOS slice for _check_dependencies 8 | function(_check_dependencies_macos) 9 | set(arch universal) 10 | set(platform macos) 11 | 12 | file(READ "${CMAKE_CURRENT_SOURCE_DIR}/buildspec.json" buildspec) 13 | 14 | set(dependencies_dir "${CMAKE_CURRENT_SOURCE_DIR}/.deps") 15 | set(prebuilt_filename "macos-deps-VERSION-ARCH_REVISION.tar.xz") 16 | set(prebuilt_destination "obs-deps-VERSION-ARCH") 17 | set(qt6_filename "macos-deps-qt6-VERSION-ARCH-REVISION.tar.xz") 18 | set(qt6_destination "obs-deps-qt6-VERSION-ARCH") 19 | set(obs-studio_filename "VERSION.tar.gz") 20 | set(obs-studio_destination "obs-studio-VERSION") 21 | set(dependencies_list prebuilt qt6 obs-studio) 22 | 23 | _check_dependencies() 24 | 25 | execute_process( 26 | COMMAND "xattr" -r -d com.apple.quarantine "${dependencies_dir}" 27 | RESULT_VARIABLE result 28 | COMMAND_ERROR_IS_FATAL ANY 29 | ) 30 | 31 | list(APPEND CMAKE_FRAMEWORK_PATH "${dependencies_dir}/Frameworks") 32 | set(CMAKE_FRAMEWORK_PATH ${CMAKE_FRAMEWORK_PATH} PARENT_SCOPE) 33 | endfunction() 34 | 35 | _check_dependencies_macos() 36 | -------------------------------------------------------------------------------- /src/ui/obs-ui-utils.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "obs-ui-utils.h" 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | // add_sources_to_list is a helper function that adds all text and media sources to the list 10 | bool add_sources_to_combobox(void *list_property, obs_source_t *source) 11 | { 12 | // add all text and media sources to the list 13 | auto source_id = obs_source_get_id(source); 14 | if (strncmp(source_id, "text_ft2_source", 15) != 0 && 15 | strncmp(source_id, "text_gdiplus", 12) != 0 && 16 | strcmp(source_id, "ffmpeg_source") != 0 && strcmp(source_id, "image_source") != 0) { 17 | return true; 18 | } 19 | 20 | QComboBox *sources = static_cast(list_property); 21 | const char *name = obs_source_get_name(source); 22 | std::string name_with_prefix; 23 | // add a prefix to the name to indicate the source type 24 | if (strncmp(source_id, "text_ft2_source", 15) == 0 || 25 | strncmp(source_id, "text_gdiplus", 12) == 0) { 26 | name_with_prefix = std::string("(Text) ").append(name); 27 | } else if (strcmp(source_id, "image_source") == 0) { 28 | name_with_prefix = std::string("(Image) ").append(name); 29 | } else if (strcmp(source_id, "ffmpeg_source") == 0) { 30 | name_with_prefix = std::string("(Media) ").append(name); 31 | } 32 | sources->addItem(name_with_prefix.c_str()); 33 | return true; 34 | } 35 | -------------------------------------------------------------------------------- /src/mapping-data.h: -------------------------------------------------------------------------------- 1 | #ifndef MAPPING_DATA_H 2 | #define MAPPING_DATA_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | const std::string none_internal_rendering = "None / Internal rendering"; 10 | const std::string save_to_setting = "Save to source settings"; 11 | const std::string file_output_rendering = "File output"; 12 | 13 | struct output_mapping { 14 | std::string name; 15 | std::string output_source; 16 | std::string template_string; 17 | std::string css_props; 18 | bool unhide_output_source = false; 19 | std::string file_path; 20 | }; 21 | 22 | struct output_mapping_data { 23 | std::vector mappings; 24 | }; 25 | 26 | std::string serialize_output_mapping_data(const output_mapping_data &data); 27 | output_mapping_data deserialize_output_mapping_data(const std::string &data); 28 | 29 | struct input_data { 30 | std::string source; 31 | bool no_empty = false; 32 | bool no_same = false; 33 | bool aggregate = false; 34 | int agg_method = -1; 35 | std::string resize_method; 36 | std::string last_obs_text_source_value; 37 | std::string aggregate_to_empty_buffer; 38 | uint64_t agg_buffer_begin_ts; 39 | }; 40 | 41 | typedef std::vector inputs_data; 42 | 43 | nlohmann::json serialize_input_mapping_data(const inputs_data &data); 44 | inputs_data deserialize_input_mapping_data(const std::string &data); 45 | 46 | #endif // MAPPING_DATA_H 47 | -------------------------------------------------------------------------------- /src/plugin-support.c.in: -------------------------------------------------------------------------------- 1 | /* 2 | URL Source 3 | Copyright (C) 2023 Roy Shilkrot roy.shil@gmail.com 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with this program. If not, see 17 | */ 18 | 19 | #include 20 | 21 | extern void blogva(int log_level, const char *format, va_list args); 22 | 23 | const char *PLUGIN_NAME = "@CMAKE_PROJECT_NAME@"; 24 | const char *PLUGIN_VERSION = "@CMAKE_PROJECT_VERSION@"; 25 | 26 | void obs_log(int log_level, const char *format, ...) 27 | { 28 | size_t length = 4 + strlen(PLUGIN_NAME) + strlen(format); 29 | 30 | char *template = malloc(length + 1); 31 | 32 | snprintf(template, length, "[%s] %s", PLUGIN_NAME, format); 33 | 34 | va_list(args); 35 | 36 | va_start(args, format); 37 | blogva(log_level, template, args); 38 | va_end(args); 39 | 40 | free(template); 41 | } 42 | -------------------------------------------------------------------------------- /src/parsers/regex.cpp: -------------------------------------------------------------------------------- 1 | #include "request-data.h" 2 | #include "plugin-support.h" 3 | #include "errors.h" 4 | 5 | #include 6 | #include 7 | 8 | struct request_data_handler_response parse_regex(struct request_data_handler_response response, 9 | const url_source_request_data *request_data) 10 | { 11 | try { 12 | if (request_data->output_regex.empty()) { 13 | response.body_parts_parsed.push_back(response.body); 14 | return response; 15 | } 16 | 17 | // Cache compiled regex patterns for better performance 18 | static thread_local std::unordered_map regex_cache; 19 | 20 | auto ®ex = regex_cache[request_data->output_regex]; 21 | if (regex_cache.find(request_data->output_regex) == regex_cache.end()) { 22 | regex = std::regex(request_data->output_regex, 23 | std::regex_constants::ECMAScript | 24 | std::regex_constants::optimize); 25 | } 26 | 27 | std::smatch match; 28 | if (std::regex_search(response.body, match, regex)) { 29 | // Get the appropriate capture group 30 | size_t group = match.size() > 1 ? 1 : 0; 31 | response.body_parts_parsed.push_back(match[group].str()); 32 | return response; 33 | } 34 | 35 | return make_fail_parse_response("No regex match found"); 36 | 37 | } catch (const std::regex_error &e) { 38 | return make_fail_parse_response(std::string("Regex error: ") + e.what()); 39 | } catch (const std::exception &e) { 40 | return make_fail_parse_response(std::string("Parse error: ") + e.what()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/parsers/jsonpointer.cpp: -------------------------------------------------------------------------------- 1 | #include "request-data.h" 2 | #include "errors.h" 3 | 4 | #include 5 | 6 | struct request_data_handler_response 7 | parse_json_pointer(struct request_data_handler_response response, 8 | const url_source_request_data *request_data) 9 | { 10 | // Parse the response as JSON 11 | nlohmann::json json; 12 | try { 13 | json = nlohmann::json::parse(response.body); 14 | } catch (nlohmann::json::parse_error &e) { 15 | return make_fail_parse_response(e.what()); 16 | } 17 | response.body_json = json; 18 | std::string parsed_output = ""; 19 | // Get the output value 20 | if (request_data->output_json_pointer != "") { 21 | try { 22 | const auto value = json.at(nlohmann::json::json_pointer( 23 | request_data->output_json_pointer)) 24 | .get(); 25 | if (value.is_string()) { 26 | parsed_output = value.get(); 27 | } else { 28 | parsed_output = value.dump(); 29 | } 30 | // remove potential prefix and postfix quotes, conversion from string 31 | if (parsed_output.size() > 1 && parsed_output.front() == '"' && 32 | parsed_output.back() == '"') { 33 | parsed_output = parsed_output.substr(1, parsed_output.size() - 2); 34 | } 35 | } catch (nlohmann::json::exception &e) { 36 | return make_fail_parse_response(e.what()); 37 | } 38 | } else { 39 | // Return the whole JSON object 40 | parsed_output = json.dump(); 41 | } 42 | response.body_parts_parsed.push_back(parsed_output); 43 | return response; 44 | } 45 | -------------------------------------------------------------------------------- /cmake/macos/resources/distribution.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | @CMAKE_PROJECT_NAME@ 10 | 11 | 12 | 13 | 14 | 15 | 16 | #@CMAKE_PROJECT_NAME@.pkg 17 | 18 | 33 | 34 | -------------------------------------------------------------------------------- /src/ui/CustomTextDocument.cpp: -------------------------------------------------------------------------------- 1 | #include "CustomTextDocument.h" 2 | 3 | #include "plugin-support.h" 4 | #include "request-data.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | // Initialize the static member 14 | QHash CustomTextDocument::imageCache; 15 | 16 | CustomTextDocument::CustomTextDocument(QObject *parent) : QTextDocument(parent) {} 17 | 18 | QVariant CustomTextDocument::loadResource(int type, const QUrl &name) 19 | { 20 | if (type == QTextDocument::ImageResource) { 21 | if (imageCache.contains(name)) { 22 | return QVariant(imageCache.value(name)); 23 | } 24 | obs_log(LOG_INFO, "fetch %s", name.toString().toStdString().c_str()); 25 | std::string mime_type; 26 | std::vector image_bytes = 27 | fetch_image(name.toString().toStdString(), mime_type); 28 | // Create a QByteArray from the std::vector 29 | QByteArray imageData(reinterpret_cast(image_bytes.data()), 30 | image_bytes.size()); 31 | QBuffer buffer(&imageData); 32 | buffer.open(QIODevice::ReadOnly); 33 | QImageReader reader(&buffer); 34 | reader.setDecideFormatFromContent(true); 35 | QImage image = reader.read(); 36 | if (!image.isNull()) { 37 | imageCache[name] = image; 38 | return image; 39 | } 40 | obs_log(LOG_ERROR, "Unable to load image from: %s", 41 | name.toString().toStdString().c_str()); 42 | return QVariant(); 43 | } 44 | return QTextDocument::loadResource(type, name); 45 | } 46 | -------------------------------------------------------------------------------- /cmake/BuildPugiXML.cmake: -------------------------------------------------------------------------------- 1 | include(ExternalProject) 2 | 3 | if(APPLE) 4 | set(PUGIXML_CMAKE_PLATFORM_OPTIONS -DCMAKE_OSX_ARCHITECTURES=x86_64$arm64) 5 | else() 6 | if(UNIX) 7 | set(PUGIXML_CMAKE_PLATFORM_OPTIONS -DCMAKE_POSITION_INDEPENDENT_CODE=ON) 8 | else() 9 | set(PUGIXML_CMAKE_PLATFORM_OPTIONS "") 10 | endif() 11 | endif() 12 | 13 | set(pugixml_lib_filename ${CMAKE_STATIC_LIBRARY_PREFIX}pugixml${CMAKE_STATIC_LIBRARY_SUFFIX}) 14 | 15 | ExternalProject_Add( 16 | pugixml_build 17 | URL https://github.com/zeux/pugixml/releases/download/v1.15/pugixml-1.15.tar.gz 18 | URL_HASH SHA256=655ade57fa703fb421c2eb9a0113b5064bddb145d415dd1f88c79353d90d511a 19 | CMAKE_GENERATOR ${CMAKE_GENERATOR} 20 | INSTALL_BYPRODUCTS /include 21 | BUILD_BYPRODUCTS /lib/${pugixml_lib_filename} 22 | CMAKE_ARGS 23 | -DCMAKE_INSTALL_PREFIX= -DBUILD_SHARED_LIBS=OFF -DPUGIXML_BUILD_TESTS=OFF 24 | -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} ${PUGIXML_CMAKE_PLATFORM_OPTIONS} -DCMAKE_INSTALL_LIBDIR=lib 25 | ) 26 | 27 | ExternalProject_Get_Property(pugixml_build INSTALL_DIR) 28 | 29 | set(pugixml_lib_location ${INSTALL_DIR}/lib/${pugixml_lib_filename}) 30 | 31 | add_library(pugixml_internal STATIC IMPORTED) 32 | add_dependencies(pugixml_internal pugixml_build) 33 | set_target_properties(pugixml_internal PROPERTIES IMPORTED_LOCATION ${pugixml_lib_location}) 34 | target_include_directories(pugixml_internal INTERFACE ${INSTALL_DIR}/include) 35 | 36 | add_library(libpugixml_internal INTERFACE) 37 | add_dependencies(libpugixml_internal pugixml_build pugixml_internal) 38 | target_link_libraries(libpugixml_internal INTERFACE pugixml_internal) 39 | -------------------------------------------------------------------------------- /src/obs-source-util.h: -------------------------------------------------------------------------------- 1 | #ifndef OBS_SOURCE_UTIL_H 2 | #define OBS_SOURCE_UTIL_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | inline bool is_obs_source_text(obs_source_t *source) 10 | { 11 | if (source == nullptr) { 12 | return false; 13 | } 14 | const auto source_id = obs_source_get_id(source); 15 | return strncmp(source_id, "text_ft2_source", 15) == 0 || 16 | strncmp(source_id, "text_gdiplus", 12) == 0; 17 | } 18 | 19 | inline bool is_obs_source_text(const std::string &source_name) 20 | { 21 | obs_source_t *source = obs_get_source_by_name(source_name.c_str()); 22 | if (source != nullptr) { 23 | const bool is_text = is_obs_source_text(source); 24 | obs_source_release(source); 25 | return is_text; 26 | } 27 | return false; 28 | } 29 | 30 | struct source_render_data { 31 | gs_texrender_t *texrender; 32 | gs_stagesurf_t *stagesurface; 33 | }; 34 | 35 | void init_source_render_data(source_render_data *tf); 36 | void destroy_source_render_data(source_render_data *tf); 37 | 38 | std::vector get_rgba_from_source_render(obs_source_t *source, source_render_data *tf, 39 | uint32_t &width, uint32_t &height, float scale); 40 | 41 | std::string convert_rgba_buffer_to_png_base64(const std::vector &rgba, uint32_t width, 42 | uint32_t height); 43 | 44 | inline bool is_valid_output_source_name(const char *output_source_name) 45 | { 46 | return output_source_name != nullptr && strcmp(output_source_name, "none") != 0 && 47 | strcmp(output_source_name, "(null)") != 0 && strcmp(output_source_name, "") != 0; 48 | } 49 | 50 | std::string get_source_name_without_prefix(const std::string &source_name); 51 | 52 | #endif // OBS_SOURCE_UTIL_H 53 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/setup_ubuntu: -------------------------------------------------------------------------------- 1 | autoload -Uz log_error log_status log_info mkcd 2 | 3 | if (( ! ${+project_root} )) { 4 | log_error "'project_root' not set. Please set before running ${0}." 5 | return 2 6 | } 7 | 8 | if (( ! ${+target} )) { 9 | log_error "'target' not set. Please set before running ${0}." 10 | return 2 11 | } 12 | 13 | pushd ${project_root} 14 | 15 | typeset -g QT_VERSION 16 | 17 | local -a apt_args=( 18 | ${CI:+-y} 19 | --no-install-recommends 20 | ) 21 | if (( _loglevel == 0 )) apt_args+=(--quiet) 22 | 23 | if (( ! (${skips[(Ie)all]} + ${skips[(Ie)deps]}) )) { 24 | log_group 'Installing obs-studio build dependencies...' 25 | 26 | local suffix 27 | if [[ ${CPUTYPE} != "${target##*-}" ]] { 28 | local -A arch_mappings=( 29 | aarch64 arm64 30 | x86_64 amd64 31 | ) 32 | 33 | suffix=":${arch_mappings[${target##*-}]}" 34 | 35 | sudo apt-get install ${apt_args} gcc-${${target##*-}//_/-}-linux-gnu g++-${${target##*-}//_/-}-linux-gnu 36 | } 37 | 38 | sudo add-apt-repository --yes ppa:obsproject/obs-studio 39 | sudo apt update 40 | 41 | sudo apt-get install ${apt_args} \ 42 | build-essential \ 43 | libgles2-mesa-dev \ 44 | libsimde-dev \ 45 | obs-studio 46 | 47 | local -a _qt_packages=() 48 | 49 | if (( QT_VERSION == 5 )) { 50 | _qt_packages+=( 51 | qtbase5-dev${suffix} 52 | libqt5svg5-dev${suffix} 53 | qtbase5-private-dev${suffix} 54 | libqt5x11extras5-dev${suffix} 55 | ) 56 | } else { 57 | _qt_packages+=( 58 | qt6-base-dev${suffix} 59 | libqt6svg6-dev${suffix} 60 | qt6-base-private-dev${suffix} 61 | ) 62 | } 63 | 64 | sudo apt-get install ${apt_args} ${_qt_packages} 65 | log_group 66 | } 67 | -------------------------------------------------------------------------------- /src/parsers/parsers.h: -------------------------------------------------------------------------------- 1 | #ifndef PARSERS_H 2 | #define PARSERS_H 3 | 4 | #include "request-data.h" 5 | 6 | struct request_data_handler_response parse_json(struct request_data_handler_response response, 7 | const url_source_request_data *request_data); 8 | 9 | struct request_data_handler_response 10 | parse_json_pointer(struct request_data_handler_response response, 11 | const url_source_request_data *request_data); 12 | 13 | struct request_data_handler_response parse_json_path(struct request_data_handler_response response, 14 | const url_source_request_data *request_data); 15 | 16 | struct request_data_handler_response parse_regex(struct request_data_handler_response response, 17 | const url_source_request_data *request_data); 18 | 19 | struct request_data_handler_response parse_xml(struct request_data_handler_response response, 20 | const url_source_request_data *request_data); 21 | 22 | struct request_data_handler_response parse_html(struct request_data_handler_response response, 23 | const url_source_request_data *request_data); 24 | 25 | struct request_data_handler_response parse_image_data(struct request_data_handler_response response, 26 | const url_source_request_data *request_data); 27 | 28 | struct request_data_handler_response parse_audio_data(struct request_data_handler_response response, 29 | const url_source_request_data *request_data); 30 | 31 | struct request_data_handler_response 32 | parse_xml_by_xquery(struct request_data_handler_response response, 33 | const url_source_request_data *request_data); 34 | 35 | struct request_data_handler_response parse_key_value(struct request_data_handler_response response, 36 | const url_source_request_data *request_data); 37 | 38 | #endif // PARSERS_H 39 | -------------------------------------------------------------------------------- /cmake/BuildLexbor.cmake: -------------------------------------------------------------------------------- 1 | include(ExternalProject) 2 | 3 | if(APPLE) 4 | set(LEXBOR_CMAKE_PLATFORM_OPTIONS -DCMAKE_OSX_ARCHITECTURES=x86_64$arm64) 5 | else() 6 | if(WIN32) 7 | add_compile_definitions(LEXBOR_STATIC=1) 8 | set(LEXBOR_CMAKE_PLATFORM_OPTIONS "-DCMAKE_C_FLAGS=/W3 /utf-8 /MP" "-DCMAKE_CXX_FLAGS=/W3 /utf-8 /MP") 9 | else() 10 | set(LEXBOR_CMAKE_PLATFORM_OPTIONS -DCMAKE_SYSTEM_NAME=Linux) 11 | endif() 12 | endif() 13 | 14 | set(lexbor_lib_filename ${CMAKE_STATIC_LIBRARY_PREFIX}lexbor_static${CMAKE_STATIC_LIBRARY_SUFFIX}) 15 | 16 | ExternalProject_Add( 17 | lexbor_build 18 | GIT_REPOSITORY https://github.com/lexbor/lexbor.git 19 | GIT_TAG v2.6.0 20 | CMAKE_GENERATOR ${CMAKE_GENERATOR} 21 | BUILD_BYPRODUCTS /lib/${lexbor_lib_filename} 22 | INSTALL_BYPRODUCTS /include 23 | CMAKE_ARGS 24 | -DCMAKE_INSTALL_PREFIX= -DLEXBOR_BUILD_SHARED=OFF -DLEXBOR_BUILD_STATIC=ON -DLEXBOR_BUILD_TESTS_CPP=OFF 25 | -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} 26 | -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} -DCMAKE_LINKER=${CMAKE_LINKER} -DCMAKE_INSTALL_MESSAGE=NEVER 27 | ${LEXBOR_CMAKE_PLATFORM_OPTIONS} 28 | ) 29 | 30 | ExternalProject_Get_Property(lexbor_build INSTALL_DIR) 31 | 32 | set(lexbor_lib_location ${INSTALL_DIR}/lib/${lexbor_lib_filename}) 33 | 34 | add_library(lexbor_internal STATIC IMPORTED) 35 | add_dependencies(lexbor_internal lexbor_build) 36 | set_target_properties(lexbor_internal PROPERTIES IMPORTED_LOCATION ${lexbor_lib_location}) 37 | target_include_directories(lexbor_internal INTERFACE ${INSTALL_DIR}/include) 38 | 39 | add_library(liblexbor_internal INTERFACE) 40 | add_dependencies(liblexbor_internal lexbor_internal lexbor_build) 41 | target_link_libraries(liblexbor_internal INTERFACE lexbor_internal) 42 | -------------------------------------------------------------------------------- /cmake/FetchWebsocketpp.cmake: -------------------------------------------------------------------------------- 1 | if(WIN32 OR APPLE) 2 | # Windows and macOS are supported by the prebuilt dependencies 3 | 4 | if(NOT buildspec) 5 | file(READ "${CMAKE_SOURCE_DIR}/buildspec.json" buildspec) 6 | endif() 7 | 8 | string(JSON version GET ${buildspec} dependencies prebuilt version) 9 | 10 | if(MSVC) 11 | set(arch ${CMAKE_GENERATOR_PLATFORM}) 12 | elseif(APPLE) 13 | set(arch universal) 14 | endif() 15 | 16 | set(deps_root "${CMAKE_SOURCE_DIR}/.deps/obs-deps-${version}-${arch}") 17 | target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE "${deps_root}/include") 18 | else() 19 | # Linux requires fetching the dependencies 20 | include(FetchContent) 21 | 22 | FetchContent_Declare( 23 | websocketpp 24 | URL https://github.com/zaphoyd/websocketpp/archive/refs/tags/0.8.2.tar.gz 25 | URL_HASH SHA256=6ce889d85ecdc2d8fa07408d6787e7352510750daa66b5ad44aacb47bea76755 26 | ) 27 | 28 | # Only download the content, don't configure or build it 29 | FetchContent_GetProperties(websocketpp) 30 | 31 | if(NOT websocketpp_POPULATED) 32 | FetchContent_Populate(websocketpp) 33 | endif() 34 | 35 | # Add WebSocket++ as an interface library 36 | add_library(websocketpp INTERFACE) 37 | target_include_directories(websocketpp INTERFACE ${websocketpp_SOURCE_DIR}) 38 | 39 | # Fetch ASIO 40 | FetchContent_Declare( 41 | asio 42 | URL https://github.com/chriskohlhoff/asio/archive/asio-1-28-0.tar.gz 43 | URL_HASH SHA256=226438b0798099ad2a202563a83571ce06dd13b570d8fded4840dbc1f97fa328 44 | ) 45 | 46 | FetchContent_MakeAvailable(websocketpp asio) 47 | 48 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE websocketpp) 49 | target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${asio_SOURCE_DIR}/asio/include/) 50 | endif() 51 | 52 | message(STATUS "WebSocket++ and ASIO have been added to the project") 53 | -------------------------------------------------------------------------------- /cmake/macos/defaults.cmake: -------------------------------------------------------------------------------- 1 | # CMake macOS defaults module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Set empty codesigning team if not specified as cache variable 6 | if(NOT CODESIGN_TEAM) 7 | set(CODESIGN_TEAM "" CACHE STRING "OBS code signing team for macOS" FORCE) 8 | 9 | # Set ad-hoc codesigning identity if not specified as cache variable 10 | if(NOT CODESIGN_IDENTITY) 11 | set(CODESIGN_IDENTITY "-" CACHE STRING "OBS code signing identity for macOS" FORCE) 12 | endif() 13 | endif() 14 | 15 | if(XCODE) 16 | include(xcode) 17 | endif() 18 | 19 | include(buildspec) 20 | 21 | # Set default deployment target to 11.0 if not set and enable selection in GUI up to 13.0 22 | if(NOT CMAKE_OSX_DEPLOYMENT_TARGET) 23 | set( 24 | CMAKE_OSX_DEPLOYMENT_TARGET 25 | 11.0 26 | CACHE STRING 27 | "Minimum macOS version to target for deployment (at runtime). Newer APIs will be weak-linked." 28 | FORCE 29 | ) 30 | endif() 31 | set_property(CACHE CMAKE_OSX_DEPLOYMENT_TARGET PROPERTY STRINGS 13.0 12.0 11.0) 32 | 33 | # Use Applications directory as default install destination 34 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 35 | set( 36 | CMAKE_INSTALL_PREFIX 37 | "$ENV{HOME}/Library/Application Support/obs-studio/plugins" 38 | CACHE STRING 39 | "Directory to install OBS after building" 40 | FORCE 41 | ) 42 | endif() 43 | 44 | # Enable find_package targets to become globally available targets 45 | set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL TRUE) 46 | # Enable RPATH support for generated binaries 47 | set(CMAKE_MACOSX_RPATH TRUE) 48 | # Use RPATHs from build tree _in_ the build tree 49 | set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) 50 | # Do not add default linker search paths to RPATH 51 | set(CMAKE_INSTALL_RPATH_USE_LINK_PATH FALSE) 52 | # Use common bundle-relative RPATH for installed targets 53 | set(CMAKE_INSTALL_RPATH "@executable_path/../Frameworks") 54 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/check_ubuntu: -------------------------------------------------------------------------------- 1 | autoload -Uz log_info log_status log_error log_debug log_warning log_group 2 | 3 | log_group 'Check Linux build requirements' 4 | log_debug 'Checking Linux distribution name and version...' 5 | 6 | # Check for Ubuntu version 22.10 or later, which have srt and librist available via apt-get 7 | typeset -g -i UBUNTU_2210_OR_LATER=0 8 | if [[ -f /etc/os_release ]] { 9 | local dist_name 10 | local dist_version 11 | read -r dist_name dist_version <<< "$(source /etc/os_release; print "${NAME} ${VERSION_ID}")" 12 | 13 | autoload -Uz is-at-least 14 | if [[ ${dist_name} == Ubuntu ]] && is-at-least 22.10 ${dist_version}; then 15 | typeset -g -i UBUNTU_2210_OR_LATER=1 16 | fi 17 | } 18 | 19 | log_debug 'Checking for apt-get...' 20 | if (( ! ${+commands[apt-get]} )) { 21 | log_error 'No apt-get command found. Please install apt' 22 | return 2 23 | } else { 24 | log_debug "Apt-get located at ${commands[apt-get]}" 25 | } 26 | 27 | local -a dependencies=("${(fA)$(<${SCRIPT_HOME}/.Aptfile)}") 28 | local -a install_list 29 | local binary 30 | 31 | sudo apt-get update -qq 32 | 33 | for dependency (${dependencies}) { 34 | local -a tokens=(${=dependency//(,|:|\')/}) 35 | 36 | if [[ ! ${tokens[1]} == 'package' ]] continue 37 | 38 | if [[ ${#tokens} -gt 2 && ${tokens[3]} == 'bin' ]] { 39 | binary=${tokens[4]} 40 | } else { 41 | binary=${tokens[2]} 42 | } 43 | 44 | if (( ! ${+commands[${binary}]} )) install_list+=(${tokens[2]}) 45 | } 46 | 47 | log_debug "List of dependencies to install: ${install_list}" 48 | if (( ${#install_list} )) { 49 | if (( ! ${+CI} )) log_warning 'Dependency installation via apt may require elevated privileges' 50 | 51 | local -a apt_args=( 52 | ${CI:+-y} 53 | --no-install-recommends 54 | ) 55 | if (( _loglevel == 0 )) apt_args+=(--quiet) 56 | 57 | sudo apt-get ${apt_args} install ${install_list} 58 | } 59 | 60 | rehash 61 | log_group 62 | -------------------------------------------------------------------------------- /cmake/linux/toolchains/aarch64-linux-clang.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_SYSTEM_NAME Linux) 2 | set(CMAKE_SYSTEM_PROCESSOR aarch64) 3 | set(CMAKE_CROSSCOMPILING TRUE) 4 | 5 | set(CMAKE_C_COMPILER /usr/bin/clang) 6 | set(CMAKE_CXX_COMPILER /usr/bin/clang++) 7 | 8 | set(CMAKE_C_COMPILER_TARGET aarch64-linux-gnu) 9 | set(CMAKE_CXX_COMPILER_TARGET aarch64-linux-gnu) 10 | 11 | set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu) 12 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 13 | set(PKG_CONFIG_EXECUTABLE /usr/bin/aarch64-linux-gnu-pkg-config CACHE FILEPATH "pkg-config executable") 14 | 15 | execute_process( 16 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-ranlib 17 | OUTPUT_VARIABLE CMAKE_RANLIB 18 | OUTPUT_STRIP_TRAILING_WHITESPACE 19 | ) 20 | 21 | execute_process( 22 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-ar 23 | OUTPUT_VARIABLE CMAKE_LLVM_AR 24 | OUTPUT_STRIP_TRAILING_WHITESPACE 25 | ) 26 | 27 | execute_process( 28 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-readelf 29 | OUTPUT_VARIABLE READELF 30 | OUTPUT_STRIP_TRAILING_WHITESPACE 31 | ) 32 | 33 | execute_process( 34 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-objcopy 35 | OUTPUT_VARIABLE CMAKE_LLVM_OBJCOPY 36 | OUTPUT_STRIP_TRAILING_WHITESPACE 37 | ) 38 | 39 | execute_process( 40 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-objdump 41 | OUTPUT_VARIABLE CMAKE_LLVM_OBJDUMP 42 | OUTPUT_STRIP_TRAILING_WHITESPACE 43 | ) 44 | 45 | set(CMAKE_AR "${CMAKE_LLVM_AR}" CACHE INTERNAL "${CMAKE_SYSTEM_NAME} ar" FORCE) 46 | set(CMAKE_OBJCOPY "${CMAKE_LLVM_OBJCOPY}" CACHE INTERNAL "${CMAKE_SYSTEM_NAME} objcopy" FORCE) 47 | set(CMAKE_OBJDUMP "${CMAKE_LLVM_OBJDUMP}" CACHE INTERNAL "${CMAKE_SYSTEM_NAME} objdump" FORCE) 48 | 49 | set(CPACK_READELF_EXECUTABLE "${READELF}") 50 | set(CPACK_OBJCOPY_EXECUTABLE "${CMAKE_LLVM_OBJCOPY}") 51 | set(CPACK_OBJDUMP_EXECUTABLE "${CMAKE_LLVM_OBJDUMP}") 52 | set(CPACK_PACKAGE_ARCHITECTURE arm64) 53 | set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE arm64) 54 | -------------------------------------------------------------------------------- /cmake/linux/toolchains/x86_64-linux-clang.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_SYSTEM_NAME Linux) 2 | set(CMAKE_SYSTEM_PROCESSOR x86_64) 3 | set(CMAKE_CROSSCOMPILING TRUE) 4 | 5 | set(CMAKE_C_COMPILER /usr/bin/clang) 6 | set(CMAKE_CXX_COMPILER /usr/bin/clang++) 7 | 8 | set(CMAKE_C_COMPILER_TARGET x86_64-linux-gnu) 9 | set(CMAKE_CXX_COMPILER_TARGET x86_64-linux-gnu) 10 | 11 | set(CMAKE_FIND_ROOT_PATH /usr/x86_64-linux-gnu) 12 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 13 | set(PKG_CONFIG_EXECUTABLE /usr/bin/x86_64-linux-gnu-pkg-config CACHE FILEPATH "pkg-config executable") 14 | 15 | execute_process( 16 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-ranlib 17 | OUTPUT_VARIABLE CMAKE_RANLIB 18 | OUTPUT_STRIP_TRAILING_WHITESPACE 19 | ) 20 | 21 | execute_process( 22 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-ar 23 | OUTPUT_VARIABLE CMAKE_LLVM_AR 24 | OUTPUT_STRIP_TRAILING_WHITESPACE 25 | ) 26 | 27 | execute_process( 28 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-readelf 29 | OUTPUT_VARIABLE READELF 30 | OUTPUT_STRIP_TRAILING_WHITESPACE 31 | ) 32 | 33 | execute_process( 34 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-objcopy 35 | OUTPUT_VARIABLE CMAKE_LLVM_OBJCOPY 36 | OUTPUT_STRIP_TRAILING_WHITESPACE 37 | ) 38 | 39 | execute_process( 40 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-objdump 41 | OUTPUT_VARIABLE CMAKE_LLVM_OBJDUMP 42 | OUTPUT_STRIP_TRAILING_WHITESPACE 43 | ) 44 | 45 | set(CMAKE_AR "${CMAKE_LLVM_AR}" CACHE INTERNAL "${CMAKE_SYSTEM_NAME} ar" FORCE) 46 | set(CMAKE_OBJCOPY "${CMAKE_LLVM_OBJCOPY}" CACHE INTERNAL "${CMAKE_SYSTEM_NAME} objcopy" FORCE) 47 | set(CMAKE_OBJDUMP "${CMAKE_LLVM_OBJDUMP}" CACHE INTERNAL "${CMAKE_SYSTEM_NAME} objdump" FORCE) 48 | 49 | set(CPACK_READELF_EXECUTABLE "${READELF}") 50 | set(CPACK_OBJCOPY_EXECUTABLE "${CMAKE_LLVM_OBJCOPY}") 51 | set(CPACK_OBJDUMP_EXECUTABLE "${CMAKE_LLVM_OBJDUMP}") 52 | set(CPACK_PACKAGE_ARCHITECTURE x86_64) 53 | set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE x86_64) 54 | -------------------------------------------------------------------------------- /src/ui/CollapseButton.h: -------------------------------------------------------------------------------- 1 | // from https://stackoverflow.com/a/68141638/5339857 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "plugin-support.h" 8 | 9 | #pragma once 10 | 11 | class CollapseButton : public QToolButton { 12 | public: 13 | CollapseButton(QWidget *parent) : QToolButton(parent), content_(nullptr) 14 | { 15 | setCheckable(true); 16 | setStyleSheet("background:none"); 17 | setIconSize(QSize(8, 8)); 18 | setFont(QApplication::font()); 19 | setToolButtonStyle(Qt::ToolButtonTextBesideIcon); 20 | setArrowType(Qt::ArrowType::RightArrow); 21 | connect(this, &QToolButton::toggled, [=](bool checked) { 22 | setArrowType(checked ? Qt::ArrowType::DownArrow 23 | : Qt::ArrowType::RightArrow); 24 | content_ != nullptr &&checked ? showContent() : hideContent(); 25 | }); 26 | } 27 | 28 | void setText(const QString &text) { QToolButton::setText(" " + text); } 29 | 30 | void setContent(QWidget *content, QWidget *parent) 31 | { 32 | assert(content != nullptr); 33 | content_ = content; 34 | auto animation_ = new QPropertyAnimation( 35 | content_, "maximumHeight"); // QObject with auto delete 36 | animation_->setStartValue(0); 37 | animation_->setEasingCurve(QEasingCurve::InOutQuad); 38 | animation_->setDuration(300); 39 | // log content->geometry().height() 40 | animation_->setEndValue(content->geometry().height() + 10); 41 | connect(animation_, &QPropertyAnimation::finished, [=]() { parent->adjustSize(); }); 42 | animator_.addAnimation(animation_); 43 | if (!isChecked()) { 44 | content->setMaximumHeight(0); 45 | } 46 | } 47 | 48 | void hideContent() 49 | { 50 | animator_.setDirection(QAbstractAnimation::Backward); 51 | animator_.start(); 52 | } 53 | 54 | void showContent() 55 | { 56 | animator_.setDirection(QAbstractAnimation::Forward); 57 | animator_.start(); 58 | } 59 | 60 | private: 61 | QWidget *content_; 62 | QParallelAnimationGroup animator_; 63 | }; 64 | -------------------------------------------------------------------------------- /cmake/common/helpers_common.cmake: -------------------------------------------------------------------------------- 1 | # CMake common helper functions module 2 | 3 | # cmake-format: off 4 | # cmake-lint: disable=C0103 5 | # cmake-format: on 6 | 7 | include_guard(GLOBAL) 8 | 9 | # check_uuid: Helper function to check for valid UUID 10 | function(check_uuid uuid_string return_value) 11 | set(valid_uuid TRUE) 12 | set( 13 | uuid_token_lengths 14 | 8 15 | 4 16 | 4 17 | 4 18 | 12 19 | ) 20 | set(token_num 0) 21 | 22 | string(REPLACE "-" ";" uuid_tokens ${uuid_string}) 23 | list(LENGTH uuid_tokens uuid_num_tokens) 24 | 25 | if(uuid_num_tokens EQUAL 5) 26 | message(DEBUG "UUID ${uuid_string} is valid with 5 tokens.") 27 | foreach(uuid_token IN LISTS uuid_tokens) 28 | list(GET uuid_token_lengths ${token_num} uuid_target_length) 29 | string(LENGTH "${uuid_token}" uuid_actual_length) 30 | if(uuid_actual_length EQUAL uuid_target_length) 31 | string(REGEX MATCH "[0-9a-fA-F]+" uuid_hex_match ${uuid_token}) 32 | if(NOT uuid_hex_match STREQUAL uuid_token) 33 | set(valid_uuid FALSE) 34 | break() 35 | endif() 36 | else() 37 | set(valid_uuid FALSE) 38 | break() 39 | endif() 40 | math(EXPR token_num "${token_num}+1") 41 | endforeach() 42 | else() 43 | set(valid_uuid FALSE) 44 | endif() 45 | message(DEBUG "UUID ${uuid_string} valid: ${valid_uuid}") 46 | set(${return_value} ${valid_uuid} PARENT_SCOPE) 47 | endfunction() 48 | 49 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/plugin-support.c.in") 50 | configure_file(src/plugin-support.c.in plugin-support.c @ONLY) 51 | add_library(plugin-support STATIC) 52 | target_sources(plugin-support PRIVATE plugin-support.c PUBLIC src/plugin-support.h) 53 | target_include_directories(plugin-support PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") 54 | if(OS_LINUX OR OS_FREEBSD OR OS_OPENBSD) 55 | # add fPIC on Linux to prevent shared object errors 56 | set_property(TARGET plugin-support PROPERTY POSITION_INDEPENDENT_CODE ON) 57 | endif() 58 | endif() 59 | -------------------------------------------------------------------------------- /cmake/windows/compilerconfig.cmake: -------------------------------------------------------------------------------- 1 | # CMake Windows compiler configuration module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(compiler_common) 6 | 7 | # CMake 3.24 introduces a bug mistakenly interpreting MSVC as supporting the '-pthread' compiler flag 8 | if(CMAKE_VERSION VERSION_EQUAL 3.24.0) 9 | set(THREADS_HAVE_PTHREAD_ARG FALSE) 10 | endif() 11 | 12 | # CMake 3.25 changed the way symbol generation is handled on Windows 13 | if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.25.0) 14 | if(CMAKE_C_COMPILER_ID STREQUAL "MSVC") 15 | set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT ProgramDatabase) 16 | else() 17 | set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT Embedded) 18 | endif() 19 | endif() 20 | 21 | message(DEBUG "Current Windows API version: ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}") 22 | if(CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION_MAXIMUM) 23 | message(DEBUG "Maximum Windows API version: ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION_MAXIMUM}") 24 | endif() 25 | 26 | if(CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION VERSION_LESS 10.0.20348) 27 | message( 28 | FATAL_ERROR 29 | "OBS requires Windows 10 SDK version 10.0.20348.0 or more recent.\n" 30 | "Please download and install the most recent Windows platform SDK." 31 | ) 32 | endif() 33 | 34 | add_compile_options( 35 | /W3 36 | /utf-8 37 | "$<$:/MP>" 38 | "$<$:/MP>" 39 | "$<$:${_obs_clang_c_options}>" 40 | "$<$:${_obs_clang_cxx_options}>" 41 | $<$>:/Gy> 42 | ) 43 | 44 | add_compile_definitions( 45 | UNICODE 46 | _UNICODE 47 | _CRT_SECURE_NO_WARNINGS 48 | _CRT_NONSTDC_NO_WARNINGS 49 | $<$:DEBUG> 50 | $<$:_DEBUG> 51 | ) 52 | 53 | # cmake-format: off 54 | add_link_options( 55 | $<$>:/OPT:REF> 56 | $<$>:/OPT:ICF> 57 | $<$>:/INCREMENTAL:NO> 58 | /DEBUG 59 | /Brepro 60 | ) 61 | # cmake-format: on 62 | 63 | if(CMAKE_COMPILE_WARNING_AS_ERROR) 64 | add_link_options(/WX) 65 | endif() 66 | -------------------------------------------------------------------------------- /cmake/macos/resources/create-package.cmake.in: -------------------------------------------------------------------------------- 1 | make_directory("$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/package/Library/Application Support/obs-studio/plugins") 2 | 3 | if(EXISTS "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin" AND NOT IS_SYMLINK "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin") 4 | file(INSTALL DESTINATION "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/package/Library/Application Support/obs-studio/plugins" 5 | TYPE DIRECTORY FILES "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin" USE_SOURCE_PERMISSIONS) 6 | 7 | if(CMAKE_INSTALL_CONFIG_NAME MATCHES "^([Rr][Ee][Ll][Ee][Aa][Ss][Ee])$" OR CMAKE_INSTALL_CONFIG_NAME MATCHES "^([Mm][Ii][Nn][Ss][Ii][Zz][Ee][Rr][Ee][Ll])$") 8 | if(EXISTS "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin.dSYM" AND NOT IS_SYMLINK "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin.dSYM") 9 | file(INSTALL DESTINATION "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/package/Library/Application Support/obs-studio/plugins" TYPE DIRECTORY FILES "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin.dSYM" USE_SOURCE_PERMISSIONS) 10 | endif() 11 | endif() 12 | endif() 13 | 14 | make_directory("$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/temp") 15 | 16 | execute_process( 17 | COMMAND /usr/bin/pkgbuild 18 | --identifier '@MACOS_BUNDLEID@' 19 | --version '@CMAKE_PROJECT_VERSION@' 20 | --root "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/package" 21 | "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/temp/@CMAKE_PROJECT_NAME@.pkg" 22 | COMMAND_ERROR_IS_FATAL ANY 23 | ) 24 | 25 | execute_process( 26 | COMMAND /usr/bin/productbuild 27 | --distribution "@CMAKE_CURRENT_BINARY_DIR@/distribution" 28 | --package-path "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/temp" 29 | "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.pkg" 30 | COMMAND_ERROR_IS_FATAL ANY) 31 | 32 | if(EXISTS "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.pkg") 33 | file(REMOVE_RECURSE "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/temp") 34 | file(REMOVE_RECURSE "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/package") 35 | endif() 36 | -------------------------------------------------------------------------------- /src/parsers/jsonpath.cpp: -------------------------------------------------------------------------------- 1 | #include "request-data.h" 2 | #include "errors.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | struct request_data_handler_response parse_json(struct request_data_handler_response response, 10 | const url_source_request_data *request_data) 11 | { 12 | UNUSED_PARAMETER(request_data); 13 | try { 14 | // Parse JSON only once and store in both formats 15 | auto json_cons = jsoncons::json::parse(response.body); 16 | response.body_json = nlohmann::json::parse(response.body); 17 | return response; 18 | } catch (const jsoncons::json_exception &e) { 19 | return make_fail_parse_response(e.what()); 20 | } catch (const nlohmann::json::exception &e) { 21 | return make_fail_parse_response(e.what()); 22 | } 23 | } 24 | 25 | struct request_data_handler_response parse_json_path(struct request_data_handler_response response, 26 | const url_source_request_data *request_data) 27 | { 28 | try { 29 | auto json = jsoncons::json::parse(response.body); 30 | response.body_json = nlohmann::json::parse(response.body); 31 | 32 | if (!request_data->output_json_path.empty()) { 33 | // Create and evaluate JSONPath expression 34 | auto value = jsoncons::jsonpath::json_query(json, 35 | request_data->output_json_path); 36 | 37 | if (value.is_array()) { 38 | response.body_parts_parsed.reserve(value.size()); 39 | for (const auto &item : value.array_range()) { 40 | response.body_parts_parsed.push_back( 41 | item.as()); 42 | } 43 | } else { 44 | response.body_parts_parsed.push_back(value.as()); 45 | } 46 | } else { 47 | response.body_parts_parsed.push_back(json.as()); 48 | } 49 | 50 | return response; 51 | 52 | } catch (const jsoncons::jsonpath::jsonpath_error &e) { 53 | return make_fail_parse_response(std::string("JSONPath error: ") + e.what()); 54 | } catch (const std::exception &e) { 55 | return make_fail_parse_response(std::string("JSON parse error: ") + e.what()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/url-source-thread.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "url-source-thread.h" 3 | #include "url-source-callbacks.h" 4 | #include "request-data.h" 5 | #include "plugin-support.h" 6 | #include "obs-source-util.h" 7 | #include "ui/text-render-helper.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | void curl_loop(struct url_source_data *usd) 15 | { 16 | obs_log(LOG_INFO, "Starting URL Source thread, update timer: %d", usd->update_timer_ms); 17 | 18 | inja::Environment env; 19 | 20 | while (usd->curl_thread_run) { 21 | // time the request 22 | uint64_t request_start_time_ns = get_time_ns(); 23 | 24 | // Send the request 25 | struct request_data_handler_response response = 26 | request_data_handler(&(usd->request_data)); 27 | if (response.status_code != URL_SOURCE_REQUEST_SUCCESS) { 28 | if (response.status_code != URL_SOURCE_REQUEST_BENIGN_ERROR_CODE) { 29 | obs_log(LOG_INFO, "Failed to send request: %s", 30 | response.error_message.c_str()); 31 | } 32 | } else { 33 | if (response.body_parts_parsed.empty()) { 34 | response.body_parts_parsed.push_back(response.body); 35 | } 36 | 37 | output_with_mapping(response, usd); 38 | } 39 | 40 | // time the request, calculate the remaining time and sleep 41 | const uint64_t request_end_time_ns = get_time_ns(); 42 | const uint64_t request_time_ns = request_end_time_ns - request_start_time_ns; 43 | const int64_t sleep_time_ms = 44 | (int64_t)(usd->update_timer_ms) - (int64_t)(request_time_ns / 1000000); 45 | if (sleep_time_ms > 0) { 46 | std::unique_lock lock(usd->curl_mutex); 47 | // Sleep for n ns as per the update timer for the remaining time 48 | usd->curl_thread_cv.wait_for(lock, 49 | std::chrono::milliseconds(sleep_time_ms)); 50 | } 51 | } 52 | obs_log(LOG_INFO, "Stopping URL Source thread"); 53 | } 54 | 55 | void stop_and_join_curl_thread(struct url_source_data *usd) 56 | { 57 | if (!usd->curl_thread_run) { 58 | // Thread is already stopped 59 | return; 60 | } 61 | usd->curl_thread_run = false; 62 | usd->curl_thread_cv.notify_all(); 63 | if (usd->curl_thread.joinable()) { 64 | usd->curl_thread.join(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.github/actions/run-gersemi/action.yaml: -------------------------------------------------------------------------------- 1 | name: Run gersemi 2 | description: Runs gersemi and checks for any changes introduced by it 3 | inputs: 4 | failCondition: 5 | description: Controls whether failed checks also fail the workflow run 6 | required: false 7 | default: never 8 | workingDirectory: 9 | description: Working directory for checks 10 | required: false 11 | default: ${{ github.workspace }} 12 | runs: 13 | using: composite 14 | steps: 15 | - name: Check Runner Operating System 🏃‍♂️ 16 | if: runner.os == 'Windows' 17 | shell: bash 18 | run: | 19 | : Check Runner Operating System 🏃‍♂️ 20 | echo "::notice::run-gersemi action requires a macOS-based or Linux-based runner." 21 | exit 2 22 | 23 | - name: Check for Changed Files ✅ 24 | uses: ./.github/actions/check-changes 25 | id: checks 26 | with: 27 | checkGlob: "'*.cmake' '*CMakeLists.txt'" 28 | diffFilter: 'ACM' 29 | 30 | - name: Install Dependencies 🛍️ 31 | if: runner.os == 'Linux' && fromJSON(steps.checks.outputs.hasChangedFiles) 32 | shell: bash 33 | run: | 34 | : Install Dependencies 🛍️ 35 | echo ::group::Install Dependencies 36 | eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 37 | echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH 38 | brew install --quiet zsh 39 | echo ::endgroup:: 40 | 41 | - name: Run gersemi 🎛️ 42 | if: fromJSON(steps.checks.outputs.hasChangedFiles) 43 | id: result 44 | shell: zsh --no-rcs --errexit --pipefail {0} 45 | working-directory: ${{ github.workspace }} 46 | env: 47 | CHANGED_FILES: ${{ steps.checks.outputs.changedFiles }} 48 | run: | 49 | : Run gersemi 🎛️ 50 | if (( ${+RUNNER_DEBUG} )) setopt XTRACE 51 | 52 | print ::group::Install gersemi 53 | brew install --quiet obsproject/tools/gersemi 54 | print ::endgroup:: 55 | 56 | print ::group::Run gersemi 57 | local -a changes=(${(s:,:)CHANGED_FILES//[\[\]\'\"]/}) 58 | ./build-aux/run-gersemi --fail-${{ inputs.failCondition }} --check ${changes} 59 | print ::endgroup:: 60 | -------------------------------------------------------------------------------- /.github/scripts/Package-Windows.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param( 3 | [ValidateSet('x64')] 4 | [string] $Target = 'x64', 5 | [ValidateSet('Debug', 'RelWithDebInfo', 'Release', 'MinSizeRel')] 6 | [string] $Configuration = 'RelWithDebInfo' 7 | ) 8 | 9 | $ErrorActionPreference = 'Stop' 10 | 11 | if ( $DebugPreference -eq 'Continue' ) { 12 | $VerbosePreference = 'Continue' 13 | $InformationPreference = 'Continue' 14 | } 15 | 16 | if ( $env:CI -eq $null ) { 17 | throw "Package-Windows.ps1 requires CI environment" 18 | } 19 | 20 | if ( ! ( [System.Environment]::Is64BitOperatingSystem ) ) { 21 | throw "Packaging script requires a 64-bit system to build and run." 22 | } 23 | 24 | if ( $PSVersionTable.PSVersion -lt '7.2.0' ) { 25 | Write-Warning 'The packaging script requires PowerShell Core 7. Install or upgrade your PowerShell version: https://aka.ms/pscore6' 26 | exit 2 27 | } 28 | 29 | function Package { 30 | trap { 31 | Write-Error $_ 32 | exit 2 33 | } 34 | 35 | $ScriptHome = $PSScriptRoot 36 | $ProjectRoot = Resolve-Path -Path "$PSScriptRoot/../.." 37 | $BuildSpecFile = "${ProjectRoot}/buildspec.json" 38 | 39 | $UtilityFunctions = Get-ChildItem -Path $PSScriptRoot/utils.pwsh/*.ps1 -Recurse 40 | 41 | foreach( $Utility in $UtilityFunctions ) { 42 | Write-Debug "Loading $($Utility.FullName)" 43 | . $Utility.FullName 44 | } 45 | 46 | $BuildSpec = Get-Content -Path ${BuildSpecFile} -Raw | ConvertFrom-Json 47 | $ProductName = $BuildSpec.name 48 | $ProductVersion = $BuildSpec.version 49 | 50 | $OutputName = "${ProductName}-${ProductVersion}-windows-${Target}" 51 | 52 | $RemoveArgs = @{ 53 | ErrorAction = 'SilentlyContinue' 54 | Path = @( 55 | "${ProjectRoot}/release/${ProductName}-*-windows-*.zip" 56 | ) 57 | } 58 | 59 | Remove-Item @RemoveArgs 60 | 61 | Log-Group "Archiving ${ProductName}..." 62 | $CompressArgs = @{ 63 | Path = (Get-ChildItem -Path "${ProjectRoot}/release/${Configuration}" -Exclude "${OutputName}*.*") 64 | CompressionLevel = 'Optimal' 65 | DestinationPath = "${ProjectRoot}/release/${OutputName}.zip" 66 | Verbose = ($Env:CI -ne $null) 67 | } 68 | Compress-Archive -Force @CompressArgs 69 | Log-Group 70 | } 71 | 72 | Package 73 | -------------------------------------------------------------------------------- /src/ui/InputsDialog.cpp: -------------------------------------------------------------------------------- 1 | #include "InputsDialog.h" 2 | 3 | #include "plugin-support.h" 4 | #include "ui_inputsdialog.h" 5 | #include "mapping-data.h" 6 | #include "obs-ui-utils.h" 7 | #include "request-data.h" 8 | #include "InputWidget.h" 9 | 10 | #include 11 | #include 12 | 13 | InputsDialog::InputsDialog(QWidget *parent) : QDialog(parent), ui(new Ui::InputsDialog) 14 | { 15 | ui->setupUi(this); 16 | 17 | connect(ui->toolButton_addinput, &QToolButton::clicked, this, &InputsDialog::addInput); 18 | connect(ui->toolButton_removeinput, &QToolButton::clicked, this, 19 | &InputsDialog::removeInput); 20 | } 21 | 22 | InputsDialog::~InputsDialog() 23 | { 24 | delete ui; 25 | } 26 | 27 | void InputsDialog::addInput() 28 | { 29 | // add a new Input widget to the tableView 30 | InputWidget *widget = new InputWidget(ui->listWidget); 31 | QListWidgetItem *item = new QListWidgetItem(ui->listWidget); 32 | 33 | item->setSizeHint(widget->sizeHint()); 34 | ui->listWidget->setItemWidget(item, widget); 35 | 36 | // enable the remove button 37 | ui->toolButton_removeinput->setEnabled(true); 38 | } 39 | 40 | void InputsDialog::removeInput() 41 | { 42 | // remove the selected Input widget from the tableView 43 | QListWidgetItem *item = ui->listWidget->currentItem(); 44 | if (item) { 45 | delete item; 46 | } 47 | 48 | // if there are no more items in the tableView, disable the remove button 49 | if (ui->listWidget->count() == 0) { 50 | ui->toolButton_removeinput->setEnabled(false); 51 | } 52 | } 53 | 54 | void InputsDialog::setInputsData(const inputs_data &data) 55 | { 56 | for (const auto &input : data) { 57 | addInput(); 58 | QWidget *widget = ui->listWidget->itemWidget( 59 | ui->listWidget->item(ui->listWidget->count() - 1)); 60 | InputWidget *inputWidget = (InputWidget *)widget; 61 | inputWidget->setInputData(input); 62 | } 63 | } 64 | 65 | inputs_data InputsDialog::getInputsDataFromUI() 66 | { 67 | inputs_data data; 68 | 69 | for (int i = 0; i < ui->listWidget->count(); i++) { 70 | QListWidgetItem *item = ui->listWidget->item(i); 71 | QWidget *widget = ui->listWidget->itemWidget(item); 72 | InputWidget *inputWidget = (InputWidget *)widget; 73 | data.push_back(inputWidget->getInputDataFromUI()); 74 | } 75 | 76 | return data; 77 | } 78 | -------------------------------------------------------------------------------- /.github/actions/run-clang-format/action.yaml: -------------------------------------------------------------------------------- 1 | name: Run clang-format 2 | description: Runs clang-format and checks for any changes introduced by it 3 | inputs: 4 | failCondition: 5 | description: Controls whether failed checks also fail the workflow run 6 | required: false 7 | default: never 8 | workingDirectory: 9 | description: Working directory for checks 10 | required: false 11 | default: ${{ github.workspace }} 12 | runs: 13 | using: composite 14 | steps: 15 | - name: Check Runner Operating System 🏃‍♂️ 16 | if: runner.os == 'Windows' 17 | shell: bash 18 | run: | 19 | : Check Runner Operating System 🏃‍♂️ 20 | echo "::notice::run-clang-format action requires a macOS-based or Linux-based runner." 21 | exit 2 22 | 23 | - name: Check for Changed Files ✅ 24 | uses: ./.github/actions/check-changes 25 | id: checks 26 | with: 27 | checkGlob: "'*.c' '*.h' '*.cpp' '*.hpp' '*.m' '*.mm'" 28 | diffFilter: 'ACM' 29 | 30 | - name: Install Dependencies 🛍️ 31 | if: runner.os == 'Linux' && fromJSON(steps.checks.outputs.hasChangedFiles) 32 | shell: bash 33 | run: | 34 | : Install Dependencies 🛍️ 35 | echo ::group::Install Dependencies 36 | eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 37 | echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH 38 | echo "/home/linuxbrew/.linuxbrew/opt/clang-format@19/bin" >> $GITHUB_PATH 39 | brew install --quiet zsh 40 | echo ::endgroup:: 41 | 42 | - name: Run clang-format 🐉 43 | if: fromJSON(steps.checks.outputs.hasChangedFiles) 44 | id: result 45 | shell: zsh --no-rcs --errexit --pipefail {0} 46 | working-directory: ${{ inputs.workingDirectory }} 47 | env: 48 | CHANGED_FILES: ${{ steps.checks.outputs.changedFiles }} 49 | run: | 50 | : Run clang-format 🐉 51 | if (( ${+RUNNER_DEBUG} )) setopt XTRACE 52 | 53 | print ::group::Install clang-format-19 54 | brew install --quiet obsproject/tools/clang-format@19 55 | print ::endgroup:: 56 | 57 | print ::group::Run clang-format-19 58 | local -a changes=(${(s:,:)CHANGED_FILES//[\[\]\'\"]/}) 59 | ./build-aux/run-clang-format --fail-${{ inputs.failCondition }} --check ${changes} 60 | print ::endgroup:: 61 | -------------------------------------------------------------------------------- /cmake/common/compiler_common.cmake: -------------------------------------------------------------------------------- 1 | # CMake common compiler options module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Set C and C++ language standards to C17 and C++17 6 | if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.21) 7 | set(CMAKE_C_STANDARD 17) 8 | else() 9 | set(CMAKE_C_STANDARD 11) 10 | endif() 11 | set(CMAKE_C_STANDARD_REQUIRED TRUE) 12 | set(CMAKE_CXX_STANDARD 17) 13 | set(CMAKE_CXX_STANDARD_REQUIRED TRUE) 14 | 15 | # Set symbols to be hidden by default for C and C++ 16 | set(CMAKE_C_VISIBILITY_PRESET hidden) 17 | set(CMAKE_CXX_VISIBILITY_PRESET hidden) 18 | set(CMAKE_VISIBILITY_INLINES_HIDDEN TRUE) 19 | 20 | # clang options for C 21 | set( 22 | _obs_clang_c_options 23 | # cmake-format: sortable 24 | -fno-strict-aliasing 25 | -Wbool-conversion 26 | -Wcomma 27 | -Wconstant-conversion 28 | -Wdeprecated-declarations 29 | -Wempty-body 30 | -Wenum-conversion 31 | -Werror=return-type 32 | -Wextra 33 | -Wformat 34 | -Wformat-security 35 | -Wfour-char-constants 36 | -Winfinite-recursion 37 | -Wint-conversion 38 | -Wnewline-eof 39 | -Wno-conversion 40 | -Wno-float-conversion 41 | -Wno-implicit-fallthrough 42 | -Wno-missing-braces 43 | -Wno-missing-field-initializers 44 | -Wno-missing-prototypes 45 | -Wno-semicolon-before-method-body 46 | -Wno-shadow 47 | -Wno-sign-conversion 48 | -Wno-strict-prototypes 49 | -Wno-trigraphs 50 | -Wno-unknown-pragmas 51 | -Wno-unused-function 52 | -Wno-unused-label 53 | -Wnon-literal-null-conversion 54 | -Wobjc-literal-conversion 55 | -Wparentheses 56 | -Wpointer-sign 57 | -Wquoted-include-in-framework-header 58 | -Wshadow 59 | -Wshorten-64-to-32 60 | -Wuninitialized 61 | -Wunreachable-code 62 | -Wunused-parameter 63 | -Wunused-value 64 | -Wunused-variable 65 | -Wvla 66 | ) 67 | 68 | # clang options for C++ 69 | set( 70 | _obs_clang_cxx_options 71 | # cmake-format: sortable 72 | ${_obs_clang_c_options} 73 | -Wconversion 74 | -Wdeprecated-implementations 75 | -Wduplicate-method-match 76 | -Wfloat-conversion 77 | -Wfour-char-constants 78 | -Wimplicit-retain-self 79 | -Winvalid-offsetof 80 | -Wmove 81 | -Wno-c++11-extensions 82 | -Wno-exit-time-destructors 83 | -Wno-implicit-atomic-properties 84 | -Wno-objc-interface-ivars 85 | -Wno-overloaded-virtual 86 | -Wrange-loop-analysis 87 | ) 88 | 89 | if(NOT DEFINED CMAKE_COMPILE_WARNING_AS_ERROR) 90 | set(CMAKE_COMPILE_WARNING_AS_ERROR ON) 91 | endif() 92 | -------------------------------------------------------------------------------- /src/mapping-data.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "mapping-data.h" 3 | #include 4 | 5 | std::string serialize_output_mapping_data(const output_mapping_data &data) 6 | { 7 | nlohmann::json j; 8 | for (const auto &mapping : data.mappings) { 9 | nlohmann::json j_mapping; 10 | j_mapping["name"] = mapping.name; 11 | j_mapping["output_source"] = mapping.output_source; 12 | j_mapping["template_string"] = mapping.template_string; 13 | j_mapping["css_props"] = mapping.css_props; 14 | j_mapping["unhide_output_source"] = mapping.unhide_output_source; 15 | j_mapping["file_path"] = mapping.file_path; 16 | j.push_back(j_mapping); 17 | } 18 | return j.dump(); 19 | } 20 | 21 | output_mapping_data deserialize_output_mapping_data(const std::string &data) 22 | { 23 | output_mapping_data result; 24 | nlohmann::json j = nlohmann::json::parse(data); 25 | for (const auto &j_mapping : j) { 26 | output_mapping mapping; 27 | mapping.name = j_mapping.value("name", ""); 28 | mapping.output_source = j_mapping.value("output_source", ""); 29 | mapping.template_string = j_mapping.value("template_string", ""); 30 | mapping.css_props = j_mapping.value("css_props", ""); 31 | mapping.unhide_output_source = j_mapping.value("unhide_output_source", false); 32 | mapping.file_path = j_mapping.value("file_path", ""); 33 | result.mappings.push_back(mapping); 34 | } 35 | return result; 36 | } 37 | 38 | nlohmann::json serialize_input_mapping_data(const inputs_data &data) 39 | { 40 | nlohmann::json j; 41 | for (const auto &input : data) { 42 | nlohmann::json j_input; 43 | j_input["source"] = input.source; 44 | j_input["no_empty"] = input.no_empty; 45 | j_input["no_same"] = input.no_same; 46 | j_input["aggregate"] = input.aggregate; 47 | j_input["agg_method"] = input.agg_method; 48 | j_input["resize_method"] = input.resize_method; 49 | j.push_back(j_input); 50 | } 51 | return j; 52 | } 53 | 54 | inputs_data deserialize_input_mapping_data(const std::string &data) 55 | { 56 | inputs_data result; 57 | nlohmann::json j = nlohmann::json::parse(data); 58 | for (const auto &j_input : j) { 59 | input_data input; 60 | input.source = j_input.value("source", ""); 61 | input.no_empty = j_input.value("no_empty", false); 62 | input.no_same = j_input.value("no_same", false); 63 | input.aggregate = j_input.value("aggregate", false); 64 | input.agg_method = j_input.value("agg_method", -1); 65 | input.resize_method = j_input.value("resize_method", ""); 66 | result.push_back(input); 67 | } 68 | return result; 69 | } 70 | -------------------------------------------------------------------------------- /cmake/windows/resources/installer-Windows.iss.in: -------------------------------------------------------------------------------- 1 | #define MyAppName "@CMAKE_PROJECT_NAME@" 2 | #define MyAppVersion "@CMAKE_PROJECT_VERSION@" 3 | #define MyAppPublisher "@PLUGIN_AUTHOR@" 4 | #define MyAppURL "@PLUGIN_WEBSITE@" 5 | 6 | [Setup] 7 | ; NOTE: The value of AppId uniquely identifies this application. 8 | ; Do not use the same AppId value in installers for other applications. 9 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 10 | AppId={{@UUID_APP@} 11 | AppName={#MyAppName} 12 | AppVersion={#MyAppVersion} 13 | AppPublisher={#MyAppPublisher} 14 | AppPublisherURL={#MyAppURL} 15 | AppSupportURL={#MyAppURL} 16 | AppUpdatesURL={#MyAppURL} 17 | DefaultDirName={code:GetDirName} 18 | DefaultGroupName={#MyAppName} 19 | OutputBaseFilename={#MyAppName}-{#MyAppVersion}-Windows-Installer 20 | Compression=lzma 21 | SolidCompression=yes 22 | DirExistsWarning=no 23 | 24 | [Languages] 25 | Name: "english"; MessagesFile: "compiler:Default.isl" 26 | 27 | [Files] 28 | Source: "..\release\Package\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 29 | Source: "..\LICENSE"; Flags: dontcopy 30 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 31 | 32 | [Icons] 33 | Name: "{group}\{cm:ProgramOnTheWeb,{#MyAppName}}"; Filename: "{#MyAppURL}" 34 | Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" 35 | 36 | [Code] 37 | procedure InitializeWizard(); 38 | var 39 | GPLText: AnsiString; 40 | Page: TOutputMsgMemoWizardPage; 41 | begin 42 | ExtractTemporaryFile('LICENSE'); 43 | LoadStringFromFile(ExpandConstant('{tmp}\LICENSE'), GPLText); 44 | Page := CreateOutputMsgMemoPage(wpWelcome, 45 | 'License Information', 'Please review the license terms before installing {#MyAppName}', 46 | 'Press Page Down to see the rest of the agreement. Once you are aware of your rights, click Next to continue.', 47 | String(GPLText) 48 | ); 49 | end; 50 | 51 | // credit where it's due : 52 | // following function come from https://github.com/Xaymar/obs-studio_amf-encoder-plugin/blob/master/%23Resources/Installer.in.iss#L45 53 | function GetDirName(Value: string): string; 54 | var 55 | InstallPath: string; 56 | begin 57 | // initialize default path, which will be returned when the following registry 58 | // key queries fail due to missing keys or for some different reason 59 | Result := '{autopf}\obs-studio'; 60 | // query the first registry value; if this succeeds, return the obtained value 61 | if RegQueryStringValue(HKLM32, 'SOFTWARE\OBS Studio', '', InstallPath) then 62 | Result := InstallPath 63 | end; 64 | 65 | -------------------------------------------------------------------------------- /.github/scripts/utils.pwsh/Install-BuildDependencies.ps1: -------------------------------------------------------------------------------- 1 | function Install-BuildDependencies { 2 | <# 3 | .SYNOPSIS 4 | Installs required build dependencies. 5 | .DESCRIPTION 6 | Additional packages might be needed for successful builds. This module contains additional 7 | dependencies available for installation via winget and, if possible, adds their locations 8 | to the environment path for future invocation. 9 | .EXAMPLE 10 | Install-BuildDependencies 11 | #> 12 | 13 | param( 14 | [string] $WingetFile = "$PSScriptRoot/.Wingetfile" 15 | ) 16 | 17 | if ( ! ( Test-Path function:Log-Warning ) ) { 18 | . $PSScriptRoot/Logger.ps1 19 | } 20 | 21 | $Prefixes = @{ 22 | 'x64' = ${Env:ProgramFiles} 23 | 'x86' = ${Env:ProgramFiles(x86)} 24 | 'arm64' = ${Env:ProgramFiles(arm)} 25 | } 26 | 27 | $Paths = $Env:Path -split [System.IO.Path]::PathSeparator 28 | 29 | $WingetOptions = @('install', '--accept-package-agreements', '--accept-source-agreements') 30 | 31 | if ( $script:Quiet ) { 32 | $WingetOptions += '--silent' 33 | } 34 | 35 | Log-Group 'Check Windows build requirements' 36 | Get-Content $WingetFile | ForEach-Object { 37 | $_, $Package, $_, $Path, $_, $Binary, $_, $Version = $_ -replace ',','' -split " +(?=(?:[^\']*\'[^\']*\')*[^\']*$)" -replace "'",'' 38 | 39 | $Prefixes.GetEnumerator() | ForEach-Object { 40 | $Prefix = $_.value 41 | $FullPath = "${Prefix}\${Path}" 42 | if ( ( Test-Path $FullPath ) -and ! ( $Paths -contains $FullPath ) ) { 43 | $Paths = @($FullPath) + $Paths 44 | $Env:Path = $Paths -join [System.IO.Path]::PathSeparator 45 | } 46 | } 47 | 48 | Log-Debug "Checking for command ${Binary}" 49 | $Found = Get-Command -ErrorAction SilentlyContinue $Binary 50 | 51 | if ( $Found ) { 52 | Log-Status "Found dependency ${Binary} as $($Found.Source)" 53 | } else { 54 | Log-Status "Installing package ${Package} $(if ( $Version -ne $null ) { "Version: ${Version}" } )" 55 | 56 | if ( $Version -ne $null ) { 57 | $WingetOptions += @('--version', ${Version}) 58 | } 59 | 60 | try { 61 | $Params = $WingetOptions + $Package 62 | 63 | winget @Params 64 | } catch { 65 | throw "Error while installing winget package ${Package}: $_" 66 | } 67 | } 68 | } 69 | Log-Group 70 | } 71 | -------------------------------------------------------------------------------- /.github/scripts/Build-Windows.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param( 3 | [ValidateSet('x64')] 4 | [string] $Target = 'x64', 5 | [ValidateSet('Debug', 'RelWithDebInfo', 'Release', 'MinSizeRel')] 6 | [string] $Configuration = 'RelWithDebInfo' 7 | ) 8 | 9 | $ErrorActionPreference = 'Stop' 10 | 11 | if ( $DebugPreference -eq 'Continue' ) { 12 | $VerbosePreference = 'Continue' 13 | $InformationPreference = 'Continue' 14 | } 15 | 16 | if ( $env:CI -eq $null ) { 17 | throw "Build-Windows.ps1 requires CI environment" 18 | } 19 | 20 | if ( ! ( [System.Environment]::Is64BitOperatingSystem ) ) { 21 | throw "A 64-bit system is required to build the project." 22 | } 23 | 24 | if ( $PSVersionTable.PSVersion -lt '7.2.0' ) { 25 | Write-Warning 'The obs-studio PowerShell build script requires PowerShell Core 7. Install or upgrade your PowerShell version: https://aka.ms/pscore6' 26 | exit 2 27 | } 28 | 29 | function Build { 30 | trap { 31 | Pop-Location -Stack BuildTemp -ErrorAction 'SilentlyContinue' 32 | Write-Error $_ 33 | Log-Group 34 | exit 2 35 | } 36 | 37 | $ScriptHome = $PSScriptRoot 38 | $ProjectRoot = Resolve-Path -Path "$PSScriptRoot/../.." 39 | 40 | $UtilityFunctions = Get-ChildItem -Path $PSScriptRoot/utils.pwsh/*.ps1 -Recurse 41 | 42 | foreach($Utility in $UtilityFunctions) { 43 | Write-Debug "Loading $($Utility.FullName)" 44 | . $Utility.FullName 45 | } 46 | 47 | Push-Location -Stack BuildTemp 48 | Ensure-Location $ProjectRoot 49 | 50 | $CmakeArgs = @('--preset', "windows-ci-${Target}") 51 | $CmakeBuildArgs = @('--build') 52 | $CmakeInstallArgs = @() 53 | 54 | if ( $DebugPreference -eq 'Continue' ) { 55 | $CmakeArgs += ('--debug-output') 56 | $CmakeBuildArgs += ('--verbose') 57 | $CmakeInstallArgs += ('--verbose') 58 | } 59 | 60 | $CmakeBuildArgs += @( 61 | '--preset', "windows-${Target}" 62 | '--config', $Configuration 63 | '--parallel' 64 | '--', '/consoleLoggerParameters:Summary', '/noLogo' 65 | ) 66 | 67 | $CmakeInstallArgs += @( 68 | '--install', "build_${Target}" 69 | '--prefix', "${ProjectRoot}/release/${Configuration}" 70 | '--config', $Configuration 71 | ) 72 | 73 | Log-Group "Configuring ${ProductName}..." 74 | Invoke-External cmake @CmakeArgs 75 | 76 | Log-Group "Building ${ProductName}..." 77 | Invoke-External cmake @CmakeBuildArgs 78 | 79 | Log-Group "Installing ${ProductName}..." 80 | Invoke-External cmake @CmakeInstallArgs 81 | 82 | Pop-Location -Stack BuildTemp 83 | Log-Group 84 | } 85 | 86 | Build 87 | -------------------------------------------------------------------------------- /.github/scripts/utils.pwsh/Expand-ArchiveExt.ps1: -------------------------------------------------------------------------------- 1 | function Expand-ArchiveExt { 2 | <# 3 | .SYNOPSIS 4 | Expands archive files. 5 | .DESCRIPTION 6 | Allows extraction of zip, 7z, gz, and xz archives. 7 | Requires tar and 7-zip to be available on the system. 8 | Archives ending with .zip but created using LZMA compression are 9 | expanded using 7-zip as a fallback. 10 | .EXAMPLE 11 | Expand-ArchiveExt -Path 12 | Expand-ArchiveExt -Path -DestinationPath 13 | #> 14 | 15 | param( 16 | [Parameter(Mandatory)] 17 | [string] $Path, 18 | [string] $DestinationPath = [System.IO.Path]::GetFileNameWithoutExtension($Path), 19 | [switch] $Force 20 | ) 21 | 22 | switch ( [System.IO.Path]::GetExtension($Path) ) { 23 | .zip { 24 | try { 25 | Expand-Archive -Path $Path -DestinationPath $DestinationPath -Force:$Force 26 | } catch { 27 | if ( Get-Command 7z ) { 28 | Invoke-External 7z x -y $Path "-o${DestinationPath}" 29 | } else { 30 | throw "Fallback utility 7-zip not found. Please install 7-zip first." 31 | } 32 | } 33 | break 34 | } 35 | { ( $_ -eq ".7z" ) -or ( $_ -eq ".exe" ) } { 36 | if ( Get-Command 7z ) { 37 | Invoke-External 7z x -y $Path "-o${DestinationPath}" 38 | } else { 39 | throw "Extraction utility 7-zip not found. Please install 7-zip first." 40 | } 41 | break 42 | } 43 | .gz { 44 | try { 45 | Invoke-External tar -x -o $DestinationPath -f $Path 46 | } catch { 47 | if ( Get-Command 7z ) { 48 | Invoke-External 7z x -y $Path "-o${DestinationPath}" 49 | } else { 50 | throw "Fallback utility 7-zip not found. Please install 7-zip first." 51 | } 52 | } 53 | break 54 | } 55 | .xz { 56 | try { 57 | Invoke-External tar -x -o $DestinationPath -f $Path 58 | } catch { 59 | if ( Get-Command 7z ) { 60 | Invoke-External 7z x -y $Path "-o${DestinationPath}" 61 | } else { 62 | throw "Fallback utility 7-zip not found. Please install 7-zip first." 63 | } 64 | } 65 | } 66 | default { 67 | throw "Unsupported archive extension provided." 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /cmake/linux/helpers.cmake: -------------------------------------------------------------------------------- 1 | # CMake Linux helper functions module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(helpers_common) 6 | 7 | # set_target_properties_plugin: Set target properties for use in obs-studio 8 | function(set_target_properties_plugin target) 9 | set(options "") 10 | set(oneValueArgs "") 11 | set(multiValueArgs PROPERTIES) 12 | cmake_parse_arguments(PARSE_ARGV 0 _STPO "${options}" "${oneValueArgs}" "${multiValueArgs}") 13 | 14 | message(DEBUG "Setting additional properties for target ${target}...") 15 | 16 | while(_STPO_PROPERTIES) 17 | list(POP_FRONT _STPO_PROPERTIES key value) 18 | set_property(TARGET ${target} PROPERTY ${key} "${value}") 19 | endwhile() 20 | 21 | set_target_properties( 22 | ${target} 23 | PROPERTIES VERSION 0 SOVERSION ${PLUGIN_VERSION} PREFIX "" 24 | ) 25 | 26 | install( 27 | TARGETS ${target} 28 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 29 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/obs-plugins 30 | ) 31 | 32 | if(TARGET plugin-support) 33 | target_link_libraries(${target} PRIVATE plugin-support) 34 | endif() 35 | 36 | target_install_resources(${target}) 37 | 38 | get_target_property(target_sources ${target} SOURCES) 39 | set(target_ui_files ${target_sources}) 40 | list(FILTER target_ui_files INCLUDE REGEX ".+\\.(ui|qrc)") 41 | source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "UI Files" FILES ${target_ui_files}) 42 | endfunction() 43 | 44 | # Helper function to add resources into bundle 45 | function(target_install_resources target) 46 | message(DEBUG "Installing resources for target ${target}...") 47 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/data") 48 | file(GLOB_RECURSE data_files "${CMAKE_CURRENT_SOURCE_DIR}/data/*") 49 | foreach(data_file IN LISTS data_files) 50 | cmake_path( 51 | RELATIVE_PATH 52 | data_file 53 | BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" 54 | OUTPUT_VARIABLE relative_path 55 | ) 56 | cmake_path(GET relative_path PARENT_PATH relative_path) 57 | target_sources(${target} PRIVATE "${data_file}") 58 | source_group("Resources/${relative_path}" FILES "${data_file}") 59 | endforeach() 60 | 61 | install( 62 | DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" 63 | DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/obs/obs-plugins/${target} 64 | USE_SOURCE_PERMISSIONS 65 | ) 66 | endif() 67 | endfunction() 68 | 69 | # Helper function to add a specific resource to a bundle 70 | function(target_add_resource target resource) 71 | message(DEBUG "Add resource '${resource}' to target ${target} at destination '${target_destination}'...") 72 | 73 | install(FILES "${resource}" DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/obs/obs-plugins/${target}) 74 | 75 | source_group("Resources" FILES "${resource}") 76 | endfunction() 77 | -------------------------------------------------------------------------------- /cmake/linux/defaults.cmake: -------------------------------------------------------------------------------- 1 | # CMake Linux defaults module 2 | 3 | # cmake-format: off 4 | # cmake-lint: disable=C0103 5 | # cmake-lint: disable=C0111 6 | # cmake-format: on 7 | 8 | include_guard(GLOBAL) 9 | 10 | include(GNUInstallDirs) 11 | 12 | # Enable find_package targets to become globally available targets 13 | set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL TRUE) 14 | 15 | set(CPACK_PACKAGE_NAME "${CMAKE_PROJECT_NAME}") 16 | set(CPACK_PACKAGE_VERSION "${CMAKE_PROJECT_VERSION}") 17 | set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CMAKE_C_LIBRARY_ARCHITECTURE}") 18 | 19 | set(CPACK_GENERATOR "DEB") 20 | set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) 21 | set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${PLUGIN_EMAIL}") 22 | set(CPACK_SET_DESTDIR ON) 23 | 24 | if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.25.0 OR NOT CMAKE_CROSSCOMPILING) 25 | set(CPACK_DEBIAN_DEBUGINFO_PACKAGE ON) 26 | endif() 27 | 28 | set(CPACK_OUTPUT_FILE_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/release") 29 | 30 | set(CPACK_SOURCE_GENERATOR "TXZ") 31 | set( 32 | CPACK_SOURCE_IGNORE_FILES 33 | # cmake-format: sortable 34 | ".*~$" 35 | \\.git/ 36 | \\.github/ 37 | \\.gitignore 38 | build_.* 39 | cmake/\\.CMakeBuildNumber 40 | release/ 41 | ) 42 | 43 | set(CPACK_VERBATIM_VARIABLES YES) 44 | set(CPACK_SOURCE_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-source") 45 | set(CPACK_ARCHIVE_THREADS 0) 46 | 47 | include(CPack) 48 | 49 | find_package(libobs QUIET) 50 | 51 | if(NOT TARGET OBS::libobs) 52 | find_package(LibObs REQUIRED) 53 | add_library(OBS::libobs ALIAS libobs) 54 | 55 | if(ENABLE_FRONTEND_API) 56 | find_path( 57 | obs-frontend-api_INCLUDE_DIR 58 | NAMES obs-frontend-api.h 59 | PATHS /usr/include /usr/local/include 60 | PATH_SUFFIXES obs 61 | ) 62 | 63 | find_library(obs-frontend-api_LIBRARY NAMES obs-frontend-api PATHS /usr/lib /usr/local/lib) 64 | 65 | if(obs-frontend-api_LIBRARY) 66 | if(NOT TARGET OBS::obs-frontend-api) 67 | if(IS_ABSOLUTE "${obs-frontend-api_LIBRARY}") 68 | add_library(OBS::obs-frontend-api UNKNOWN IMPORTED) 69 | set_property(TARGET OBS::obs-frontend-api PROPERTY IMPORTED_LOCATION "${obs-frontend-api_LIBRARY}") 70 | else() 71 | add_library(OBS::obs-frontend-api INTERFACE IMPORTED) 72 | set_property(TARGET OBS::obs-frontend-api PROPERTY IMPORTED_LIBNAME "${obs-frontend-api_LIBRARY}") 73 | endif() 74 | 75 | set_target_properties( 76 | OBS::obs-frontend-api 77 | PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${obs-frontend-api_INCLUDE_DIR}" 78 | ) 79 | endif() 80 | endif() 81 | endif() 82 | 83 | macro(find_package) 84 | if(NOT "${ARGV0}" STREQUAL libobs AND NOT "${ARGV0}" STREQUAL obs-frontend-api) 85 | _find_package(${ARGV}) 86 | endif() 87 | endmacro() 88 | endif() 89 | -------------------------------------------------------------------------------- /cmake/linux/compilerconfig.cmake: -------------------------------------------------------------------------------- 1 | # CMake Linux compiler configuration module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(ccache) 6 | include(compiler_common) 7 | 8 | option(ENABLE_COMPILER_TRACE "Enable Clang time-trace (required Clang and Ninja)" OFF) 9 | mark_as_advanced(ENABLE_COMPILER_TRACE) 10 | 11 | # gcc options for C 12 | set( 13 | _obs_gcc_c_options 14 | # cmake-format: sortable 15 | -fno-strict-aliasing 16 | -fopenmp-simd 17 | -Wdeprecated-declarations 18 | -Wempty-body 19 | -Wenum-conversion 20 | -Werror=return-type 21 | -Wextra 22 | -Wformat 23 | -Wformat-security 24 | -Wno-conversion 25 | -Wno-error=conversion 26 | -Wno-float-conversion 27 | -Wno-implicit-fallthrough 28 | -Wno-missing-braces 29 | -Wno-missing-field-initializers 30 | -Wno-shadow 31 | -Wno-sign-conversion 32 | -Wno-trigraphs 33 | -Wno-unknown-pragmas 34 | -Wno-unused-function 35 | -Wno-unused-label 36 | -Wparentheses 37 | -Wuninitialized 38 | -Wunreachable-code 39 | -Wunused-parameter 40 | -Wunused-value 41 | -Wunused-variable 42 | -Wvla 43 | ) 44 | 45 | # gcc options for C++ 46 | set( 47 | _obs_gcc_cxx_options # cmake-format: sortable 48 | ${_obs_gcc_c_options} 49 | -Winvalid-offsetof 50 | -Wno-overloaded-virtual 51 | ) 52 | 53 | add_compile_options( 54 | -fopenmp-simd 55 | "$<$:${_obs_gcc_c_options}>" 56 | "$<$:-Wint-conversion;-Wno-missing-prototypes;-Wno-strict-prototypes;-Wpointer-sign>" 57 | "$<$:${_obs_gcc_cxx_options}>" 58 | "$<$:${_obs_clang_c_options}>" 59 | "$<$:${_obs_clang_cxx_options}>" 60 | ) 61 | 62 | # Add support for color diagnostics and CMake switch for warnings as errors to CMake < 3.24 63 | if(CMAKE_VERSION VERSION_LESS 3.24.0) 64 | add_compile_options($<$:-fcolor-diagnostics> $<$:-fcolor-diagnostics>) 65 | else() 66 | set(CMAKE_COLOR_DIAGNOSTICS ON) 67 | endif() 68 | 69 | if(CMAKE_CXX_COMPILER_ID STREQUAL GNU) 70 | # Disable false-positive warning in GCC 12.1.0 and later 71 | add_compile_options(-Wno-error=maybe-uninitialized) 72 | 73 | # Add warning for infinite recursion (added in GCC 12) 74 | if(CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 12.0.0) 75 | add_compile_options(-Winfinite-recursion) 76 | endif() 77 | endif() 78 | 79 | # Enable compiler and build tracing (requires Ninja generator) 80 | if(ENABLE_COMPILER_TRACE AND CMAKE_GENERATOR STREQUAL "Ninja") 81 | add_compile_options($<$:-ftime-trace> $<$:-ftime-trace>) 82 | else() 83 | set(ENABLE_COMPILER_TRACE OFF CACHE STRING "Enable Clang time-trace (required Clang and Ninja)" FORCE) 84 | endif() 85 | 86 | add_compile_definitions($<$:DEBUG> $<$:_DEBUG> SIMDE_ENABLE_OPENMP) 87 | -------------------------------------------------------------------------------- /cmake/common/bootstrap.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16...3.26) 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Enable automatic PUSH and POP of policies to parent scope 6 | if(POLICY CMP0011) 7 | cmake_policy(SET CMP0011 NEW) 8 | endif() 9 | 10 | # Enable distinction between Clang and AppleClang 11 | if(POLICY CMP0025) 12 | cmake_policy(SET CMP0025 NEW) 13 | endif() 14 | 15 | # Enable strict checking of "break()" usage 16 | if(POLICY CMP0055) 17 | cmake_policy(SET CMP0055 NEW) 18 | endif() 19 | 20 | # Honor visibility presets for all target types (executable, shared, module, static) 21 | if(POLICY CMP0063) 22 | cmake_policy(SET CMP0063 NEW) 23 | endif() 24 | 25 | # Disable export function calls to populate package registry by default 26 | if(POLICY CMP0090) 27 | cmake_policy(SET CMP0090 NEW) 28 | endif() 29 | 30 | # Prohibit in-source builds 31 | if("${CMAKE_CURRENT_BINARY_DIR}" STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}") 32 | message( 33 | FATAL_ERROR 34 | "In-source builds are not supported. " 35 | "Specify a build directory via 'cmake -S -B ' instead." 36 | ) 37 | file(REMOVE_RECURSE "${CMAKE_CURRENT_SOURCE_DIR}/CMakeCache.txt" "${CMAKE_CURRENT_SOURCE_DIR}/CMakeFiles") 38 | endif() 39 | 40 | # Use folders for source file organization with IDE generators (Visual Studio/Xcode) 41 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 42 | 43 | # Add common module directories to default search path 44 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/common") 45 | 46 | file(READ "${CMAKE_CURRENT_SOURCE_DIR}/buildspec.json" buildspec) 47 | 48 | # cmake-format: off 49 | string(JSON _name GET ${buildspec} name) 50 | string(JSON _website GET ${buildspec} website) 51 | string(JSON _author GET ${buildspec} author) 52 | string(JSON _email GET ${buildspec} email) 53 | string(JSON _version GET ${buildspec} version) 54 | string(JSON _bundleId GET ${buildspec} platformConfig macos bundleId) 55 | string(JSON _windowsAppUUID GET ${buildspec} uuids windowsApp) 56 | # cmake-format: on 57 | 58 | set(PLUGIN_AUTHOR ${_author}) 59 | set(PLUGIN_WEBSITE ${_website}) 60 | set(PLUGIN_EMAIL ${_email}) 61 | set(PLUGIN_VERSION ${_version}) 62 | set(MACOS_BUNDLEID ${_bundleId}) 63 | 64 | include(buildnumber) 65 | include(osconfig) 66 | 67 | # Allow selection of common build types via UI 68 | if(NOT CMAKE_BUILD_TYPE) 69 | set( 70 | CMAKE_BUILD_TYPE 71 | "RelWithDebInfo" 72 | CACHE STRING 73 | "OBS build type [Release, RelWithDebInfo, Debug, MinSizeRel]" 74 | FORCE 75 | ) 76 | set_property( 77 | CACHE CMAKE_BUILD_TYPE 78 | PROPERTY STRINGS Release RelWithDebInfo Debug MinSizeRel 79 | ) 80 | endif() 81 | 82 | # Disable exports automatically going into the CMake package registry 83 | set(CMAKE_EXPORT_PACKAGE_REGISTRY FALSE) 84 | # Enable default inclusion of targets' source and binary directory 85 | set(CMAKE_INCLUDE_CURRENT_DIR TRUE) 86 | -------------------------------------------------------------------------------- /src/ui/text-render-helper.cpp: -------------------------------------------------------------------------------- 1 | #include "CustomTextDocument.h" 2 | #include 3 | 4 | const QString template_text = R"( 5 | 6 | 7 | 8 | 87 | 88 | 89 | 90 |
{{text}}
91 | 92 | 93 | 94 | )"; 95 | 96 | /** 97 | * Render text to a buffer using QTextDocument 98 | * @param text Text to render 99 | * @param width Output width 100 | * @param height Output height 101 | * @param data Output buffer, user must free 102 | * @param css_props CSS properties to apply to the text 103 | */ 104 | void render_text_with_qtextdocument(const std::string &text, uint32_t &width, uint32_t &height, 105 | uint8_t **data, const std::string &css_props) 106 | { 107 | // apply response in template 108 | QString html = QString(template_text) 109 | .replace("{{text}}", QString::fromStdString(text)) 110 | .replace("{{css_props}}", QString::fromStdString(css_props)); 111 | CustomTextDocument textDocument; 112 | textDocument.setHtml(html); 113 | textDocument.setTextWidth(width); 114 | 115 | QPixmap pixmap(textDocument.size().toSize()); 116 | pixmap.fill(Qt::transparent); 117 | QPainter painter; 118 | painter.begin(&pixmap); 119 | painter.setCompositionMode(QPainter::CompositionMode_Source); 120 | 121 | // render text 122 | textDocument.drawContents(&painter); 123 | 124 | painter.setCompositionMode(QPainter::CompositionMode_DestinationIn); 125 | painter.end(); 126 | 127 | // save pixmap to buffer 128 | QImage image = pixmap.toImage(); 129 | // get width and height 130 | width = image.width(); 131 | height = image.height(); 132 | // allocate output buffer (RGBA), user must free 133 | *data = (uint8_t *)bzalloc(width * height * 4); 134 | // copy image data to output buffer 135 | memcpy(*data, image.bits(), width * height * 4); 136 | } 137 | -------------------------------------------------------------------------------- /.github/actions/check-changes/action.yaml: -------------------------------------------------------------------------------- 1 | name: Check For Changed Files 2 | description: Checks for changed files compared to specific git reference and glob expression 3 | inputs: 4 | baseRef: 5 | description: Git reference to check against 6 | required: false 7 | ref: 8 | description: Git reference to check with 9 | required: false 10 | default: HEAD 11 | checkGlob: 12 | description: Glob expression to limit check to specific files 13 | required: false 14 | useFallback: 15 | description: Use fallback compare against prior commit 16 | required: false 17 | default: 'true' 18 | diffFilter: 19 | description: git diff-filter string to use 20 | required: false 21 | default: '' 22 | outputs: 23 | hasChangedFiles: 24 | value: ${{ steps.checks.outputs.hasChangedFiles }} 25 | description: True if specified files were changed in comparison to specified git reference 26 | changedFiles: 27 | value: ${{ steps.checks.outputs.changedFiles }} 28 | description: List of changed files 29 | runs: 30 | using: composite 31 | steps: 32 | - name: Check For Changed Files ✅ 33 | shell: bash 34 | id: checks 35 | env: 36 | GIT_BASE_REF: ${{ inputs.baseRef }} 37 | GIT_REF: ${{ inputs.ref }} 38 | GITHUB_EVENT_FORCED: ${{ github.event.forced }} 39 | GITHUB_REF_BEFORE: ${{ github.event.before }} 40 | USE_FALLBACK: ${{ inputs.useFallback }} 41 | DIFF_FILTER: ${{ inputs.diffFilter }} 42 | run: | 43 | : Check for Changed Files ✅ 44 | if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi 45 | shopt -s extglob 46 | shopt -s dotglob 47 | 48 | if [[ "${GIT_BASE_REF}" ]]; then 49 | if ! git cat-file -e "${GIT_BASE_REF}" &> /dev/null; then 50 | echo "::warning::Provided base reference ${GIT_BASE_REF} is invalid" 51 | if [[ "${USE_FALLBACK}" == 'true' ]]; then 52 | GIT_BASE_REF='HEAD~1' 53 | fi 54 | fi 55 | else 56 | if ! git cat-file -e ${GITHUB_REF_BEFORE} &> /dev/null; then 57 | GITHUB_REF_BEFORE='4b825dc642cb6eb9a060e54bf8d69288fbee4904' 58 | fi 59 | 60 | GIT_BASE_REF='HEAD~1' 61 | case "${GITHUB_EVENT_NAME}" in 62 | pull_request) GIT_BASE_REF="origin/${GITHUB_BASE_REF}" ;; 63 | push) if [[ "${GITHUB_EVENT_FORCED}" != 'true' ]]; then GIT_BASE_REF="${GITHUB_REF_BEFORE}"; fi ;; 64 | *) ;; 65 | esac 66 | fi 67 | 68 | changes=($(git diff --name-only --diff-filter="${DIFF_FILTER}" ${GIT_BASE_REF} ${GIT_REF} -- ${{ inputs.checkGlob }})) 69 | 70 | if (( ${#changes[@]} )); then 71 | file_string="${changes[*]}" 72 | echo "hasChangedFiles=true" >> $GITHUB_OUTPUT 73 | echo "changedFiles=[\"${file_string// /\",\"}\"]" >> $GITHUB_OUTPUT 74 | else 75 | echo "hasChangedFiles=false" >> $GITHUB_OUTPUT 76 | echo "changedFiles=[]" >> GITHUB_OUTPUT 77 | fi 78 | -------------------------------------------------------------------------------- /src/parsers/xml.cpp: -------------------------------------------------------------------------------- 1 | #include "request-data.h" 2 | #include "plugin-support.h" 3 | #include "errors.h" 4 | 5 | #include 6 | #include 7 | 8 | struct request_data_handler_response parse_xml(struct request_data_handler_response response, 9 | const url_source_request_data *request_data) 10 | { 11 | pugi::xml_document doc; 12 | 13 | try { 14 | // Use load_buffer instead of load_string for better performance with known size 15 | pugi::xml_parse_result result = 16 | doc.load_buffer(response.body.c_str(), response.body.size(), 17 | pugi::parse_default, pugi::encoding_utf8); 18 | 19 | if (!result) { 20 | obs_log(LOG_INFO, "Failed to parse XML response: %s", result.description()); 21 | return make_fail_parse_response(result.description()); 22 | } 23 | 24 | if (doc.empty()) { 25 | return make_fail_parse_response("Empty XML document"); 26 | } 27 | 28 | if (!request_data->output_xpath.empty()) { 29 | obs_log(LOG_INFO, "Parsing XML with XPath: %s", 30 | request_data->output_xpath.c_str()); 31 | 32 | // Compile XPath expression once for better performance 33 | pugi::xpath_query query(request_data->output_xpath.c_str()); 34 | pugi::xpath_node_set nodes = query.evaluate_node_set(doc); 35 | 36 | if (nodes.empty()) { 37 | return make_fail_parse_response("XPath query returned no results"); 38 | } 39 | 40 | // Get all matching nodes 41 | for (const auto &node : nodes) { 42 | response.body_parts_parsed.push_back(node.node().text().get()); 43 | } 44 | } else { 45 | // Return the whole XML object 46 | response.body_parts_parsed.push_back(response.body); 47 | } 48 | 49 | return response; 50 | 51 | } catch (const pugi::xpath_exception &e) { 52 | obs_log(LOG_INFO, "XPath evaluation failed: %s", e.what()); 53 | return make_fail_parse_response(e.what()); 54 | } catch (const std::exception &e) { 55 | obs_log(LOG_INFO, "XML parsing failed: %s", e.what()); 56 | return make_fail_parse_response(e.what()); 57 | } 58 | } 59 | 60 | struct request_data_handler_response 61 | parse_xml_by_xquery(struct request_data_handler_response response, 62 | const url_source_request_data *request_data) 63 | { 64 | pugi::xml_document doc; 65 | 66 | try { 67 | pugi::xml_parse_result result = 68 | doc.load_buffer(response.body.c_str(), response.body.size(), 69 | pugi::parse_default, pugi::encoding_utf8); 70 | 71 | if (!result) { 72 | return make_fail_parse_response(result.description()); 73 | } 74 | 75 | if (!request_data->output_xquery.empty()) { 76 | pugi::xpath_query query(request_data->output_xquery.c_str()); 77 | std::string result = query.evaluate_string(doc); 78 | response.body_parts_parsed.push_back(std::move(result)); 79 | } else { 80 | response.body_parts_parsed.push_back(response.body); 81 | } 82 | 83 | return response; 84 | 85 | } catch (const pugi::xpath_exception &e) { 86 | return make_fail_parse_response(e.what()); 87 | } catch (const std::exception &e) { 88 | return make_fail_parse_response(e.what()); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16...3.26) 2 | 3 | include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/common/bootstrap.cmake" NO_POLICY_SCOPE) 4 | 5 | project(${_name} VERSION ${_version}) 6 | 7 | option(ENABLE_FRONTEND_API "Use obs-frontend-api for UI functionality" OFF) 8 | option(ENABLE_QT "Use Qt functionality" OFF) 9 | 10 | include(compilerconfig) 11 | include(defaults) 12 | include(helpers) 13 | 14 | add_library(${CMAKE_PROJECT_NAME} MODULE) 15 | 16 | find_package(libobs REQUIRED) 17 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE OBS::libobs) 18 | 19 | if(ENABLE_FRONTEND_API) 20 | find_package(obs-frontend-api REQUIRED) 21 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE OBS::obs-frontend-api) 22 | endif() 23 | 24 | if(ENABLE_QT) 25 | find_package(Qt6 COMPONENTS Widgets Core) 26 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE Qt6::Core Qt6::Widgets) 27 | target_compile_options( 28 | ${CMAKE_PROJECT_NAME} 29 | PRIVATE $<$:-Wno-quoted-include-in-framework-header -Wno-comma> 30 | ) 31 | set_target_properties( 32 | ${CMAKE_PROJECT_NAME} 33 | PROPERTIES AUTOMOC ON AUTOUIC ON AUTORCC ON 34 | ) 35 | endif() 36 | 37 | set(USE_SYSTEM_CURL OFF CACHE STRING "Use system cURL") 38 | 39 | set(USE_SYSTEM_PUGIXML OFF CACHE STRING "Use system pugixml") 40 | 41 | if(USE_SYSTEM_CURL) 42 | find_package(CURL REQUIRED) 43 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE "${CURL_LIBRARIES}") 44 | target_include_directories(${CMAKE_PROJECT_NAME} SYSTEM PUBLIC "${CURL_INCLUDE_DIRS}") 45 | else() 46 | include(cmake/BuildMyCurl.cmake) 47 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE libcurl) 48 | endif() 49 | 50 | if(USE_SYSTEM_PUGIXML) 51 | find_package(pugixml REQUIRED) 52 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE pugixml) 53 | else() 54 | include(cmake/BuildPugiXML.cmake) 55 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE libpugixml_internal) 56 | endif() 57 | 58 | include(cmake/BuildJSONCONS.cmake) 59 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE jsoncons) 60 | 61 | include(cmake/BuildInja.cmake) 62 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE inja) 63 | 64 | include(cmake/BuildLexbor.cmake) 65 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE liblexbor_internal) 66 | 67 | include(cmake/FetchWebsocketpp.cmake) 68 | 69 | target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE vendor/nlohmann-json) 70 | 71 | target_sources( 72 | ${CMAKE_PROJECT_NAME} 73 | PRIVATE 74 | src/plugin-main.c 75 | src/obs-source-util.cpp 76 | src/mapping-data.cpp 77 | src/request-data.cpp 78 | src/websocket-client.cpp 79 | src/ui/CustomTextDocument.cpp 80 | src/ui/RequestBuilder.cpp 81 | src/ui/text-render-helper.cpp 82 | src/ui/outputmapping.cpp 83 | src/ui/InputsDialog.cpp 84 | src/ui/InputWidget.cpp 85 | src/ui/obs-ui-utils.cpp 86 | src/url-source-callbacks.cpp 87 | src/url-source-info.c 88 | src/url-source-thread.cpp 89 | src/url-source.cpp 90 | ) 91 | add_subdirectory(src/parsers) 92 | 93 | set_target_properties_plugin(${CMAKE_PROJECT_NAME} PROPERTIES OUTPUT_NAME ${_name}) 94 | -------------------------------------------------------------------------------- /src/ui/InputWidget.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "InputWidget.h" 3 | #include "plugin-support.h" 4 | #include "ui_inputwidget.h" 5 | #include "obs-ui-utils.h" 6 | #include "obs-source-util.h" 7 | #include "request-data.h" 8 | 9 | InputWidget::InputWidget(QWidget *parent) : QWidget(parent), ui(new Ui::InputWidget) 10 | { 11 | ui->setupUi(this); 12 | 13 | // populate list of OBS text sources 14 | obs_enum_sources(add_sources_to_combobox, ui->obsTextSourceComboBox); 15 | 16 | auto setObsTextSourceValueOptionsVisibility = [=]() { 17 | // Hide the options if no OBS text source is selected 18 | ui->widget_inputValueOptions->setEnabled( 19 | ui->obsTextSourceComboBox->currentIndex() != 0); 20 | // adjust the size of the dialog to fit the content 21 | this->adjustSize(); 22 | }; 23 | connect(ui->obsTextSourceComboBox, &QComboBox::currentTextChanged, this, 24 | setObsTextSourceValueOptionsVisibility); 25 | 26 | auto setAggTargetEnabled = [=]() { 27 | ui->comboBox_aggTarget->setEnabled(ui->aggToTarget->isChecked()); 28 | }; 29 | connect(ui->aggToTarget, &QCheckBox::toggled, this, setAggTargetEnabled); 30 | 31 | auto inputSourceSelected = [=]() { 32 | // if the source is a media source, show the resize option, otherwise hide it 33 | auto current_data = ui->obsTextSourceComboBox->currentData(); 34 | bool hide_resize_option = true; 35 | if (current_data.isValid()) { 36 | const std::string source_name = current_data.toString().toStdString(); 37 | hide_resize_option = is_obs_source_text(source_name); 38 | } 39 | ui->comboBox_resizeInput->setVisible(!hide_resize_option); 40 | ui->label_resizeInput->setVisible(!hide_resize_option); 41 | }; 42 | connect(ui->obsTextSourceComboBox, &QComboBox::currentTextChanged, this, 43 | inputSourceSelected); 44 | } 45 | 46 | InputWidget::~InputWidget() 47 | { 48 | delete ui; 49 | } 50 | 51 | void InputWidget::setInputData(const input_data &input) 52 | { 53 | ui->obsTextSourceComboBox->setCurrentText(input.source.c_str()); 54 | if (input.aggregate) { 55 | ui->comboBox_aggTarget->setCurrentIndex(input.agg_method); 56 | // enable ui->comboBox_aggTarget 57 | ui->comboBox_aggTarget->setEnabled(true); 58 | } 59 | ui->aggToTarget->setChecked(input.aggregate); 60 | ui->comboBox_resizeInput->setCurrentText(input.resize_method.c_str()); 61 | ui->obsTextSourceEnabledCheckBox->setChecked(input.no_empty); 62 | ui->obsTextSourceSkipSameCheckBox->setChecked(input.no_same); 63 | } 64 | 65 | input_data InputWidget::getInputDataFromUI() 66 | { 67 | input_data input; 68 | 69 | input.source = ui->obsTextSourceComboBox->currentText().toUtf8().constData(); 70 | if (ui->aggToTarget->isChecked()) { 71 | input.agg_method = ui->comboBox_aggTarget->currentIndex(); 72 | } else { 73 | input.agg_method = URL_SOURCE_AGG_TARGET_NONE; 74 | } 75 | input.aggregate = ui->aggToTarget->isChecked(); 76 | input.resize_method = ui->comboBox_resizeInput->currentText().toUtf8().constData(); 77 | input.no_empty = ui->obsTextSourceEnabledCheckBox->isChecked(); 78 | input.no_same = ui->obsTextSourceSkipSameCheckBox->isChecked(); 79 | 80 | return input; 81 | } 82 | -------------------------------------------------------------------------------- /cmake/BuildMyCurl.cmake: -------------------------------------------------------------------------------- 1 | include(FetchContent) 2 | 3 | set(LibCurl_VERSION "8.4.0-3") 4 | set(LibCurl_BASEURL "https://github.com/obs-ai/obs-ai-libcurl-dep/releases/download/${LibCurl_VERSION}") 5 | 6 | if(${CMAKE_BUILD_TYPE} STREQUAL Release OR ${CMAKE_BUILD_TYPE} STREQUAL RelWithDebInfo) 7 | set(LibCurl_BUILD_TYPE Release) 8 | else() 9 | set(LibCurl_BUILD_TYPE Debug) 10 | endif() 11 | 12 | if(APPLE) 13 | if(LibCurl_BUILD_TYPE STREQUAL Release) 14 | set(LibCurl_URL "${LibCurl_BASEURL}/libcurl-macos-8.4.0-3-Release.tar.gz") 15 | set(LibCurl_HASH SHA256=5ef7bfed2c2bca17ba562aede6a3c3eb465b8d7516cff86ca0f0d0337de951e1) 16 | else() 17 | set(LibCurl_URL "${LibCurl_BASEURL}/libcurl-macos-8.4.0-3-Debug.tar.gz") 18 | set(LibCurl_HASH SHA256=da0801168eac5103e6b27bfd0f56f82e0617f85e4e6c69f476071dbba273403b) 19 | endif() 20 | elseif(MSVC) 21 | if(LibCurl_BUILD_TYPE STREQUAL Release) 22 | set(LibCurl_URL "${LibCurl_BASEURL}/libcurl-windows-8.4.0-3-Release.zip") 23 | set(LibCurl_HASH SHA256=bf4d4cd7d741712a2913df0994258d11aabe22c9a305c9f336ed59e76f351adf) 24 | else() 25 | set(LibCurl_URL "${LibCurl_BASEURL}/libcurl-windows-8.4.0-3-Debug.zip") 26 | set(LibCurl_HASH SHA256=9fe20e677ffb0d7dd927b978d532e23574cdb1923e2d2ca7c5e42f1fff2ec529) 27 | endif() 28 | else() 29 | if(LibCurl_BUILD_TYPE STREQUAL Release) 30 | set(LibCurl_URL "${LibCurl_BASEURL}/libcurl-linux-8.4.0-3-Release.tar.gz") 31 | set(LibCurl_HASH SHA256=f2cd80b7d3288fe5b4c90833bcbf0bde7c9574bc60eddb13015df19c5a09f56b) 32 | else() 33 | set(LibCurl_URL "${LibCurl_BASEURL}/libcurl-linux-8.4.0-3-Debug.tar.gz") 34 | set(LibCurl_HASH SHA256=6a41d3daef98acc3172b3702118dcf1cccbde923f3836ed2f4f3ed7301e47b8b) 35 | endif() 36 | endif() 37 | 38 | FetchContent_Declare(libcurl_fetch URL ${LibCurl_URL} URL_HASH ${LibCurl_HASH}) 39 | FetchContent_MakeAvailable(libcurl_fetch) 40 | 41 | if(MSVC) 42 | set(libcurl_fetch_lib_location "${libcurl_fetch_SOURCE_DIR}/lib/libcurl.lib") 43 | set(libcurl_fetch_link_libs "\$;\$;\$;\$") 44 | else() 45 | find_package(ZLIB REQUIRED) 46 | set(libcurl_fetch_lib_location "${libcurl_fetch_SOURCE_DIR}/lib/libcurl.a") 47 | if(UNIX AND NOT APPLE) 48 | find_package(OpenSSL REQUIRED) 49 | set(libcurl_fetch_link_libs "\$;\$;\$") 50 | else() 51 | set( 52 | libcurl_fetch_link_libs 53 | "-framework SystemConfiguration;-framework Security;-framework CoreFoundation;-framework CoreServices;ZLIB::ZLIB" 54 | ) 55 | endif() 56 | endif() 57 | 58 | # Create imported target 59 | add_library(libcurl STATIC IMPORTED) 60 | 61 | set_target_properties( 62 | libcurl 63 | PROPERTIES 64 | INTERFACE_COMPILE_DEFINITIONS "CURL_STATICLIB" 65 | INTERFACE_INCLUDE_DIRECTORIES "${libcurl_fetch_SOURCE_DIR}/include" 66 | INTERFACE_LINK_LIBRARIES "${libcurl_fetch_link_libs}" 67 | ) 68 | set_property(TARGET libcurl APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE) 69 | set_target_properties( 70 | libcurl 71 | PROPERTIES IMPORTED_LINK_INTERFACE_LANGUAGES_RELEASE "C" IMPORTED_LOCATION_RELEASE ${libcurl_fetch_lib_location} 72 | ) 73 | -------------------------------------------------------------------------------- /src/ui/inputsdialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | InputsDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 634 10 | 313 11 | 12 | 13 | 14 | Inputs 15 | 16 | 17 | 18 | 19 | 20 | 21 | 0 22 | 0 23 | 24 | 25 | 26 | 27 | 111 28 | 111 29 | 30 | 31 | 32 | 33 | 0 34 | 35 | 36 | 0 37 | 38 | 39 | 0 40 | 41 | 42 | 0 43 | 44 | 45 | 0 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | + 57 | 58 | 59 | 60 | 61 | 62 | 63 | false 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | Qt::Horizontal 80 | 81 | 82 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | buttonBox 92 | accepted() 93 | InputsDialog 94 | accept() 95 | 96 | 97 | 248 98 | 254 99 | 100 | 101 | 157 102 | 274 103 | 104 | 105 | 106 | 107 | buttonBox 108 | rejected() 109 | InputsDialog 110 | reject() 111 | 112 | 113 | 316 114 | 260 115 | 116 | 117 | 286 118 | 274 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /cmake/macos/compilerconfig.cmake: -------------------------------------------------------------------------------- 1 | # CMake macOS compiler configuration module 2 | 3 | include_guard(GLOBAL) 4 | 5 | option(ENABLE_COMPILER_TRACE "Enable clang time-trace" OFF) 6 | mark_as_advanced(ENABLE_COMPILER_TRACE) 7 | 8 | if(NOT XCODE) 9 | message(FATAL_ERROR "Building OBS Studio on macOS requires Xcode generator.") 10 | endif() 11 | 12 | include(ccache) 13 | include(compiler_common) 14 | 15 | add_compile_options("$<$>:-fopenmp-simd>") 16 | 17 | # Ensure recent enough Xcode and platform SDK 18 | function(check_sdk_requirements) 19 | set(obs_macos_minimum_sdk 15.0) # Keep in sync with Xcode 20 | set(obs_macos_minimum_xcode 16.0) # Keep in sync with SDK 21 | execute_process( 22 | COMMAND xcrun --sdk macosx --show-sdk-platform-version 23 | OUTPUT_VARIABLE obs_macos_current_sdk 24 | RESULT_VARIABLE result 25 | OUTPUT_STRIP_TRAILING_WHITESPACE 26 | ) 27 | if(NOT result EQUAL 0) 28 | message( 29 | FATAL_ERROR 30 | "Failed to fetch macOS SDK version. " 31 | "Ensure that the macOS SDK is installed and that xcode-select points at the Xcode developer directory." 32 | ) 33 | endif() 34 | message(DEBUG "macOS SDK version: ${obs_macos_current_sdk}") 35 | if(obs_macos_current_sdk VERSION_LESS obs_macos_minimum_sdk) 36 | message( 37 | FATAL_ERROR 38 | "Your macOS SDK version (${obs_macos_current_sdk}) is too low. " 39 | "The macOS ${obs_macos_minimum_sdk} SDK (Xcode ${obs_macos_minimum_xcode}) is required to build OBS." 40 | ) 41 | endif() 42 | execute_process(COMMAND xcrun --find xcodebuild OUTPUT_VARIABLE obs_macos_xcodebuild RESULT_VARIABLE result) 43 | if(NOT result EQUAL 0) 44 | message( 45 | FATAL_ERROR 46 | "Xcode was not found. " 47 | "Ensure you have installed Xcode and that xcode-select points at the Xcode developer directory." 48 | ) 49 | endif() 50 | message(DEBUG "Path to xcodebuild binary: ${obs_macos_xcodebuild}") 51 | if(XCODE_VERSION VERSION_LESS obs_macos_minimum_xcode) 52 | message( 53 | FATAL_ERROR 54 | "Your Xcode version (${XCODE_VERSION}) is too low. Xcode ${obs_macos_minimum_xcode} is required to build OBS." 55 | ) 56 | endif() 57 | endfunction() 58 | 59 | check_sdk_requirements() 60 | 61 | # Enable dSYM generator for release builds 62 | string(APPEND CMAKE_C_FLAGS_RELEASE " -g") 63 | string(APPEND CMAKE_CXX_FLAGS_RELEASE " -g") 64 | string(APPEND CMAKE_OBJC_FLAGS_RELEASE " -g") 65 | string(APPEND CMAKE_OBJCXX_FLAGS_RELEASE " -g") 66 | 67 | # Default ObjC compiler options used by Xcode: 68 | # 69 | # * -Wno-implicit-atomic-properties 70 | # * -Wno-objc-interface-ivars 71 | # * -Warc-repeated-use-of-weak 72 | # * -Wno-arc-maybe-repeated-use-of-weak 73 | # * -Wimplicit-retain-self 74 | # * -Wduplicate-method-match 75 | # * -Wshadow 76 | # * -Wfloat-conversion 77 | # * -Wobjc-literal-conversion 78 | # * -Wno-selector 79 | # * -Wno-strict-selector-match 80 | # * -Wundeclared-selector 81 | # * -Wdeprecated-implementations 82 | # * -Wprotocol 83 | # * -Werror=block-capture-autoreleasing 84 | # * -Wrange-loop-analysis 85 | 86 | # Default ObjC++ compiler options used by Xcode: 87 | # 88 | # * -Wno-non-virtual-dtor 89 | 90 | add_compile_definitions( 91 | $<$>:$<$:DEBUG>> 92 | $<$>:$<$:_DEBUG>> 93 | $<$>:SIMDE_ENABLE_OPENMP> 94 | ) 95 | 96 | if(ENABLE_COMPILER_TRACE) 97 | add_compile_options( 98 | $<$>:-ftime-trace> 99 | "$<$:SHELL:-Xfrontend -debug-time-expression-type-checking>" 100 | "$<$:SHELL:-Xfrontend -debug-time-function-bodies>" 101 | ) 102 | add_link_options(LINKER:-print_statistics) 103 | endif() 104 | -------------------------------------------------------------------------------- /.github/actions/build-plugin/action.yaml: -------------------------------------------------------------------------------- 1 | name: Set up and build plugin 2 | description: Builds the plugin for specified architecture and build config 3 | inputs: 4 | target: 5 | description: Target architecture for dependencies 6 | required: true 7 | config: 8 | description: Build configuration 9 | required: false 10 | default: RelWithDebInfo 11 | codesign: 12 | description: Enable codesigning (macOS only) 13 | required: false 14 | default: 'false' 15 | codesignIdent: 16 | description: Developer ID for application codesigning (macOS only) 17 | required: false 18 | default: '-' 19 | codesignTeam: 20 | description: Team ID for application codesigning (macOS only) 21 | required: false 22 | default: '' 23 | workingDirectory: 24 | description: Working directory for packaging 25 | required: false 26 | default: ${{ github.workspace }} 27 | runs: 28 | using: composite 29 | steps: 30 | - name: Run macOS Build 31 | if: runner.os == 'macOS' 32 | shell: zsh --no-rcs --errexit --pipefail {0} 33 | working-directory: ${{ inputs.workingDirectory }} 34 | env: 35 | CCACHE_DIR: ${{ inputs.workingDirectory }}/.ccache 36 | CODESIGN_IDENT: ${{ inputs.codesignIdent }} 37 | CODESIGN_TEAM: ${{ inputs.codesignTeam }} 38 | run: | 39 | : Run macOS Build 40 | 41 | local -a build_args=(--config ${{ inputs.config }}) 42 | if (( ${+RUNNER_DEBUG} )) build_args+=(--debug) 43 | 44 | if [[ '${{ inputs.codesign }}' == 'true' ]] build_args+=(--codesign) 45 | 46 | .github/scripts/build-macos ${build_args} 47 | 48 | - name: Install Dependencies 🛍️ 49 | if: runner.os == 'Linux' 50 | shell: bash 51 | run: | 52 | : Install Dependencies 🛍️ 53 | echo ::group::Install Dependencies 54 | eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 55 | echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH 56 | brew install --quiet zsh 57 | echo ::endgroup:: 58 | 59 | - name: Run Ubuntu Build 60 | if: runner.os == 'Linux' 61 | shell: zsh --no-rcs --errexit --pipefail {0} 62 | working-directory: ${{ inputs.workingDirectory }} 63 | env: 64 | CCACHE_DIR: ${{ inputs.workingDirectory }}/.ccache 65 | run: | 66 | : Run Ubuntu Build 67 | 68 | local -a build_args=( 69 | --target ubuntu-${{ inputs.target }} 70 | --config ${{ inputs.config }} 71 | ) 72 | if (( ${+RUNNER_DEBUG} )) build_args+=(--debug) 73 | 74 | .github/scripts/build-ubuntu ${build_args} 75 | 76 | - name: Run Windows Build 77 | if: runner.os == 'Windows' 78 | shell: pwsh 79 | run: | 80 | # Run Windows Build 81 | if ( $Env:RUNNER_DEBUG -ne $null ) { 82 | Set-PSDebug -Trace 1 83 | } 84 | 85 | $BuildArgs = @{ 86 | Target = '${{ inputs.target }}' 87 | Configuration = '${{ inputs.config }}' 88 | } 89 | 90 | .github/scripts/Build-Windows.ps1 @BuildArgs 91 | 92 | - name: Create Summary 📊 93 | if: contains(fromJSON('["Linux", "macOS"]'),runner.os) 94 | shell: zsh --no-rcs --errexit --pipefail {0} 95 | env: 96 | CCACHE_DIR: ${{ inputs.workingDirectory }}/.ccache 97 | run: | 98 | : Create Summary 📊 99 | 100 | local -a ccache_data 101 | if (( ${+RUNNER_DEBUG} )) { 102 | setopt XTRACE 103 | ccache_data=("${(fA)$(ccache -s -vv)}") 104 | } else { 105 | ccache_data=("${(fA)$(ccache -s)}") 106 | } 107 | 108 | print '### ${{ runner.os }} Ccache Stats (${{ inputs.target }})' >> $GITHUB_STEP_SUMMARY 109 | print '```' >> $GITHUB_STEP_SUMMARY 110 | for line (${ccache_data}) { 111 | print ${line} >> $GITHUB_STEP_SUMMARY 112 | } 113 | print '```' >> $GITHUB_STEP_SUMMARY 114 | -------------------------------------------------------------------------------- /cmake/macos/helpers.cmake: -------------------------------------------------------------------------------- 1 | # CMake macOS helper functions module 2 | 3 | # cmake-format: off 4 | # cmake-lint: disable=C0103 5 | # cmake-lint: disable=C0307 6 | # cmake-format: on 7 | 8 | include_guard(GLOBAL) 9 | 10 | include(helpers_common) 11 | 12 | # set_target_properties_obs: Set target properties for use in obs-studio 13 | function(set_target_properties_plugin target) 14 | set(options "") 15 | set(oneValueArgs "") 16 | set(multiValueArgs PROPERTIES) 17 | cmake_parse_arguments(PARSE_ARGV 0 _STPO "${options}" "${oneValueArgs}" "${multiValueArgs}") 18 | 19 | message(DEBUG "Setting additional properties for target ${target}...") 20 | 21 | while(_STPO_PROPERTIES) 22 | list(POP_FRONT _STPO_PROPERTIES key value) 23 | set_property(TARGET ${target} PROPERTY ${key} "${value}") 24 | endwhile() 25 | 26 | string(TIMESTAMP CURRENT_YEAR "%Y") 27 | set_target_properties( 28 | ${target} 29 | PROPERTIES 30 | BUNDLE TRUE 31 | BUNDLE_EXTENSION plugin 32 | XCODE_ATTRIBUTE_PRODUCT_NAME ${target} 33 | XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER ${MACOS_BUNDLEID} 34 | XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION ${PLUGIN_BUILD_NUMBER} 35 | XCODE_ATTRIBUTE_MARKETING_VERSION ${PLUGIN_VERSION} 36 | XCODE_ATTRIBUTE_GENERATE_INFOPLIST_FILE YES 37 | XCODE_ATTRIBUTE_INFOPLIST_FILE "" 38 | XCODE_ATTRIBUTE_INFOPLIST_KEY_CFBundleDisplayName ${target} 39 | XCODE_ATTRIBUTE_INFOPLIST_KEY_NSHumanReadableCopyright "(c) ${CURRENT_YEAR} ${PLUGIN_AUTHOR}" 40 | XCODE_ATTRIBUTE_INSTALL_PATH "$(USER_LIBRARY_DIR)/Application Support/obs-studio/plugins" 41 | ) 42 | 43 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/entitlements.plist") 44 | set_target_properties( 45 | ${target} 46 | PROPERTIES XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/entitlements.plist" 47 | ) 48 | endif() 49 | 50 | if(TARGET plugin-support) 51 | target_link_libraries(${target} PRIVATE plugin-support) 52 | endif() 53 | 54 | target_install_resources(${target}) 55 | 56 | get_target_property(target_sources ${target} SOURCES) 57 | set(target_ui_files ${target_sources}) 58 | list(FILTER target_ui_files INCLUDE REGEX ".+\\.(ui|qrc)") 59 | source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "UI Files" FILES ${target_ui_files}) 60 | 61 | install(TARGETS ${target} LIBRARY DESTINATION .) 62 | install(FILES "$.dsym" CONFIGURATIONS Release DESTINATION . OPTIONAL) 63 | 64 | configure_file(cmake/macos/resources/distribution.in "${CMAKE_CURRENT_BINARY_DIR}/distribution" @ONLY) 65 | configure_file(cmake/macos/resources/create-package.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/create-package.cmake" @ONLY) 66 | install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/create-package.cmake") 67 | endfunction() 68 | 69 | # target_install_resources: Helper function to add resources into bundle 70 | function(target_install_resources target) 71 | message(DEBUG "Installing resources for target ${target}...") 72 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/data") 73 | file(GLOB_RECURSE data_files "${CMAKE_CURRENT_SOURCE_DIR}/data/*") 74 | foreach(data_file IN LISTS data_files) 75 | cmake_path( 76 | RELATIVE_PATH 77 | data_file 78 | BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" 79 | OUTPUT_VARIABLE relative_path 80 | ) 81 | cmake_path(GET relative_path PARENT_PATH relative_path) 82 | target_sources(${target} PRIVATE "${data_file}") 83 | set_property(SOURCE "${data_file}" PROPERTY MACOSX_PACKAGE_LOCATION "Resources/${relative_path}") 84 | source_group("Resources/${relative_path}" FILES "${data_file}") 85 | endforeach() 86 | endif() 87 | endfunction() 88 | 89 | # target_add_resource: Helper function to add a specific resource to a bundle 90 | function(target_add_resource target resource) 91 | message(DEBUG "Add resource ${resource} to target ${target} at destination ${destination}...") 92 | target_sources(${target} PRIVATE "${resource}") 93 | set_property(SOURCE "${resource}" PROPERTY MACOSX_PACKAGE_LOCATION Resources) 94 | source_group("Resources" FILES "${resource}") 95 | endfunction() 96 | -------------------------------------------------------------------------------- /.github/actions/package-plugin/action.yaml: -------------------------------------------------------------------------------- 1 | name: Package plugin 2 | description: Packages the plugin for specified architecture and build config. 3 | inputs: 4 | target: 5 | description: Build target for dependencies 6 | required: true 7 | config: 8 | description: Build configuration 9 | required: false 10 | default: RelWithDebInfo 11 | codesign: 12 | description: Enable codesigning (macOS only) 13 | required: false 14 | default: 'false' 15 | notarize: 16 | description: Enable notarization (macOS only) 17 | required: false 18 | default: 'false' 19 | codesignIdent: 20 | description: Developer ID for application codesigning (macOS only) 21 | required: false 22 | default: '-' 23 | installerIdent: 24 | description: Developer ID for installer package codesigning (macOS only) 25 | required: false 26 | default: '' 27 | codesignTeam: 28 | description: Developer team for codesigning (macOS only) 29 | required: false 30 | default: '' 31 | codesignUser: 32 | description: Apple ID username for notarization (macOS only) 33 | required: false 34 | default: '' 35 | codesignPass: 36 | description: Apple ID password for notarization (macOS only) 37 | required: false 38 | default: '' 39 | package: 40 | description: Create Windows or macOS installation package 41 | required: false 42 | default: 'false' 43 | workingDirectory: 44 | description: Working directory for packaging 45 | required: false 46 | default: ${{ github.workspace }} 47 | runs: 48 | using: composite 49 | steps: 50 | - name: Run macOS Packaging 51 | if: runner.os == 'macOS' 52 | shell: zsh --no-rcs --errexit --pipefail {0} 53 | working-directory: ${{ inputs.workingDirectory }} 54 | env: 55 | CODESIGN_IDENT: ${{ inputs.codesignIdent }} 56 | CODESIGN_IDENT_INSTALLER: ${{ inputs.installerIdent }} 57 | CODESIGN_TEAM: ${{ inputs.codesignTeam }} 58 | CODESIGN_IDENT_USER: ${{ inputs.codesignUser }} 59 | CODESIGN_IDENT_PASS: ${{ inputs.codesignPass }} 60 | run: | 61 | : Run macOS Packaging 62 | 63 | local -a package_args=(--config ${{ inputs.config }}) 64 | if (( ${+RUNNER_DEBUG} )) package_args+=(--debug) 65 | 66 | if [[ '${{ inputs.codesign }}' == 'true' ]] package_args+=(--codesign) 67 | if [[ '${{ inputs.notarize }}' == 'true' ]] package_args+=(--notarize) 68 | if [[ '${{ inputs.package }}' == 'true' ]] package_args+=(--package) 69 | 70 | .github/scripts/package-macos ${package_args} 71 | 72 | - name: Install Dependencies 🛍️ 73 | if: runner.os == 'Linux' 74 | shell: bash 75 | run: | 76 | : Install Dependencies 🛍️ 77 | echo ::group::Install Dependencies 78 | eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 79 | echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH 80 | brew install --quiet zsh 81 | echo ::endgroup:: 82 | 83 | - name: Run Ubuntu Packaging 84 | if: runner.os == 'Linux' 85 | shell: zsh --no-rcs --errexit --pipefail {0} 86 | working-directory: ${{ inputs.workingDirectory }} 87 | run: | 88 | : Run Ubuntu Packaging 89 | package_args=( 90 | --target ubuntu-${{ inputs.target }} 91 | --config ${{ inputs.config }} 92 | ) 93 | if (( ${+RUNNER_DEBUG} )) build_args+=(--debug) 94 | 95 | if [[ '${{ inputs.package }}' == 'true' ]] package_args+=(--package) 96 | 97 | .github/scripts/package-ubuntu ${package_args} 98 | 99 | - name: Run Windows Packaging 100 | if: runner.os == 'Windows' 101 | shell: pwsh 102 | run: | 103 | # Run Windows Packaging 104 | if ( $Env:RUNNER_DEBUG -ne $null ) { 105 | Set-PSDebug -Trace 1 106 | } 107 | 108 | $PackageArgs = @{ 109 | Target = '${{ inputs.target }}' 110 | Configuration = '${{ inputs.config }}' 111 | } 112 | 113 | .github/scripts/Package-Windows.ps1 @PackageArgs 114 | -------------------------------------------------------------------------------- /.github/scripts/utils.pwsh/Logger.ps1: -------------------------------------------------------------------------------- 1 | function Log-Debug { 2 | [CmdletBinding()] 3 | param( 4 | [Parameter(Mandatory,ValueFromPipeline)] 5 | [ValidateNotNullOrEmpty()] 6 | [string[]] $Message 7 | ) 8 | 9 | Process { 10 | foreach($m in $Message) { 11 | Write-Debug "$(if ( $env:CI -ne $null ) { '::debug::' })$m" 12 | } 13 | } 14 | } 15 | 16 | function Log-Verbose { 17 | [CmdletBinding()] 18 | param( 19 | [Parameter(Mandatory,ValueFromPipeline)] 20 | [ValidateNotNullOrEmpty()] 21 | [string[]] $Message 22 | ) 23 | 24 | Process { 25 | foreach($m in $Message) { 26 | Write-Verbose $m 27 | } 28 | } 29 | } 30 | 31 | function Log-Warning { 32 | [CmdletBinding()] 33 | param( 34 | [Parameter(Mandatory,ValueFromPipeline)] 35 | [ValidateNotNullOrEmpty()] 36 | [string[]] $Message 37 | ) 38 | 39 | Process { 40 | foreach($m in $Message) { 41 | Write-Warning "$(if ( $env:CI -ne $null ) { '::warning::' })$m" 42 | } 43 | } 44 | } 45 | 46 | function Log-Error { 47 | [CmdletBinding()] 48 | param( 49 | [Parameter(Mandatory,ValueFromPipeline)] 50 | [ValidateNotNullOrEmpty()] 51 | [string[]] $Message 52 | ) 53 | 54 | Process { 55 | foreach($m in $Message) { 56 | Write-Error "$(if ( $env:CI -ne $null ) { '::error::' })$m" 57 | } 58 | } 59 | } 60 | 61 | function Log-Information { 62 | [CmdletBinding()] 63 | param( 64 | [Parameter(Mandatory,ValueFromPipeline)] 65 | [ValidateNotNullOrEmpty()] 66 | [string[]] $Message 67 | ) 68 | 69 | Process { 70 | if ( ! ( $script:Quiet ) ) { 71 | $StageName = $( if ( $script:StageName -ne $null ) { $script:StageName } else { '' }) 72 | $Icon = ' =>' 73 | 74 | foreach($m in $Message) { 75 | Write-Host -NoNewLine -ForegroundColor Blue " ${StageName} $($Icon.PadRight(5)) " 76 | Write-Host "${m}" 77 | } 78 | } 79 | } 80 | } 81 | 82 | function Log-Group { 83 | [CmdletBinding()] 84 | param( 85 | [Parameter(ValueFromPipeline)] 86 | [string[]] $Message 87 | ) 88 | 89 | Process { 90 | if ( $Env:CI -ne $null ) { 91 | if ( $script:LogGroup ) { 92 | Write-Output '::endgroup::' 93 | $script:LogGroup = $false 94 | } 95 | 96 | if ( $Message.count -ge 1 ) { 97 | Write-Output "::group::$($Message -join ' ')" 98 | $script:LogGroup = $true 99 | } 100 | } else { 101 | if ( $Message.count -ge 1 ) { 102 | Log-Information $Message 103 | } 104 | } 105 | } 106 | } 107 | 108 | function Log-Status { 109 | [CmdletBinding()] 110 | param( 111 | [Parameter(Mandatory,ValueFromPipeline)] 112 | [ValidateNotNullOrEmpty()] 113 | [string[]] $Message 114 | ) 115 | 116 | Process { 117 | if ( ! ( $script:Quiet ) ) { 118 | $StageName = $( if ( $StageName -ne $null ) { $StageName } else { '' }) 119 | $Icon = ' >' 120 | 121 | foreach($m in $Message) { 122 | Write-Host -NoNewLine -ForegroundColor Green " ${StageName} $($Icon.PadRight(5)) " 123 | Write-Host "${m}" 124 | } 125 | } 126 | } 127 | } 128 | 129 | function Log-Output { 130 | [CmdletBinding()] 131 | param( 132 | [Parameter(Mandatory,ValueFromPipeline)] 133 | [ValidateNotNullOrEmpty()] 134 | [string[]] $Message 135 | ) 136 | 137 | Process { 138 | if ( ! ( $script:Quiet ) ) { 139 | $StageName = $( if ( $script:StageName -ne $null ) { $script:StageName } else { '' }) 140 | $Icon = '' 141 | 142 | foreach($m in $Message) { 143 | Write-Output " ${StageName} $($Icon.PadRight(5)) ${m}" 144 | } 145 | } 146 | } 147 | } 148 | 149 | $Columns = (Get-Host).UI.RawUI.WindowSize.Width - 5 150 | -------------------------------------------------------------------------------- /src/parsers/binary-data.cpp: -------------------------------------------------------------------------------- 1 | #include "errors.h" 2 | #include "request-data.h" 3 | #include "plugin-support.h" 4 | #include "string-util.h" 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | std::string normalizeFilename(std::string filename) 14 | { 15 | const std::string invalidChars = "<>:\"/\\|?* "; // Add other invalid characters as needed 16 | 17 | // Replace invalid characters 18 | for (char &c : filename) { 19 | if (invalidChars.find(c) != std::string::npos) { 20 | c = '_'; 21 | } 22 | } 23 | 24 | // Length check (example for 255 characters) 25 | if (filename.length() > 255) { 26 | filename = filename.substr(0, 255); 27 | } 28 | 29 | // Convert to lower case (optional) 30 | std::transform(filename.begin(), filename.end(), filename.begin(), ::tolower); 31 | 32 | return filename; 33 | } 34 | 35 | std::string save_to_temp_file(const std::vector &data, const std::string &extension, 36 | const std::string &source_name) 37 | { 38 | // check if the config folder exists if it doesn't exist, create it. 39 | char *config_path = obs_module_config_path(""); 40 | if (!std::filesystem::exists(config_path)) { 41 | if (!std::filesystem::create_directory(config_path)) { 42 | obs_log(LOG_ERROR, "Failed to create config directory %s", config_path); 43 | bfree(config_path); 44 | return ""; 45 | } 46 | } 47 | bfree(config_path); 48 | 49 | // normlize the source name to remove any invalid characters by replacing them with underscores 50 | std::string normalized_source_name = normalizeFilename(source_name); 51 | 52 | // append the extension to the file name 53 | std::string file_name = "temp_" + normalized_source_name + "." + extension; 54 | char *temp_file_path = obs_module_config_path(file_name.c_str()); 55 | std::string temp_file_path_str(temp_file_path); 56 | bfree(temp_file_path); 57 | 58 | std::ofstream temp_file(temp_file_path_str, std::ios::binary); 59 | temp_file.write((const char *)data.data(), data.size()); 60 | temp_file.close(); 61 | return temp_file_path_str; 62 | } 63 | 64 | struct request_data_handler_response parse_image_data(struct request_data_handler_response response, 65 | const url_source_request_data *request_data) 66 | { 67 | // find the image type from the content type on the response.headers map 68 | std::string content_type = response.headers["content-type"]; 69 | std::string image_type = content_type.substr(content_type.find("/") + 1); 70 | image_type = trim(image_type); 71 | 72 | // if the image type is not supported, return an error 73 | if (image_type != "png" && image_type != "jpg" && image_type != "jpeg" && 74 | image_type != "gif") { 75 | return make_fail_parse_response("Unsupported image type: " + image_type); 76 | } 77 | 78 | // save the image to a temporary file 79 | std::string temp_file_path = 80 | save_to_temp_file(response.body_bytes, image_type, request_data->source_name); 81 | response.body = temp_file_path; 82 | 83 | return response; 84 | } 85 | 86 | struct request_data_handler_response parse_audio_data(struct request_data_handler_response response, 87 | const url_source_request_data *request_data) 88 | { 89 | // find the audio type from the content type on the response.headers map 90 | std::string content_type = response.headers["content-type"]; 91 | std::string audio_type = content_type.substr(content_type.find("/") + 1); 92 | audio_type = trim(audio_type); 93 | 94 | // if the audio type is not supported, return an error 95 | if (!(audio_type == "mp3" || audio_type == "mpeg" || audio_type == "wav" || 96 | audio_type == "ogg" || audio_type == "flac" || audio_type == "aac")) { 97 | return make_fail_parse_response("Unsupported audio type: " + audio_type); 98 | } 99 | 100 | // if audio type is mpeg - change the audio type to mp3 for the filename extension 101 | if (audio_type == "mpeg") { 102 | audio_type = "mp3"; 103 | } 104 | 105 | // save the audio to a temporary file 106 | std::string temp_file_path = 107 | save_to_temp_file(response.body_bytes, audio_type, request_data->source_name); 108 | response.body = temp_file_path; 109 | 110 | return response; 111 | } 112 | -------------------------------------------------------------------------------- /.github/workflows/push.yaml: -------------------------------------------------------------------------------- 1 | name: Push 2 | run-name: ${{ github.ref_name }} push run 🚀 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | - 'release/**' 9 | tags: 10 | - '*' 11 | permissions: 12 | contents: write 13 | jobs: 14 | check-format: 15 | name: Check Formatting 🔍 16 | if: github.ref_name == 'master' || github.ref_name == 'main' 17 | uses: ./.github/workflows/check-format.yaml 18 | permissions: 19 | contents: read 20 | 21 | build-project: 22 | name: Build Project 🧱 23 | uses: ./.github/workflows/build-project.yaml 24 | secrets: inherit 25 | permissions: 26 | contents: read 27 | 28 | create-release: 29 | name: Create Release 🛫 30 | if: github.ref_type == 'tag' 31 | runs-on: ubuntu-24.04 32 | needs: build-project 33 | defaults: 34 | run: 35 | shell: bash 36 | steps: 37 | - name: Check Release Tag ☑️ 38 | id: check 39 | run: | 40 | : Check Release Tag ☑️ 41 | if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi 42 | shopt -s extglob 43 | 44 | case "${GITHUB_REF_NAME}" in 45 | +([0-9]).+([0-9]).+([0-9]) ) 46 | echo 'validTag=true' >> $GITHUB_OUTPUT 47 | echo 'prerelease=false' >> $GITHUB_OUTPUT 48 | echo "version=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT 49 | ;; 50 | +([0-9]).+([0-9]).+([0-9])-@(beta|rc)*([0-9]) ) 51 | echo 'validTag=true' >> $GITHUB_OUTPUT 52 | echo 'prerelease=true' >> $GITHUB_OUTPUT 53 | echo "version=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT 54 | ;; 55 | *) echo 'validTag=false' >> $GITHUB_OUTPUT ;; 56 | esac 57 | 58 | - name: Download Build Artifacts 📥 59 | uses: actions/download-artifact@v4 60 | if: fromJSON(steps.check.outputs.validTag) 61 | id: download 62 | 63 | - name: Rename Files 🏷️ 64 | if: fromJSON(steps.check.outputs.validTag) 65 | run: | 66 | : Rename Files 🏷️ 67 | if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi 68 | shopt -s extglob 69 | shopt -s nullglob 70 | 71 | root_dir="$(pwd)" 72 | commit_hash="${GITHUB_SHA:0:9}" 73 | 74 | variants=( 75 | 'windows-x64;zip|exe' 76 | 'macos-universal;tar.xz|pkg' 77 | 'ubuntu-24.04-x86_64;tar.xz|deb|ddeb' 78 | 'sources;tar.xz' 79 | ) 80 | 81 | for variant_data in "${variants[@]}"; do 82 | IFS=';' read -r variant suffix <<< "${variant_data}" 83 | 84 | candidates=(*-${variant}-${commit_hash}/@(*|*-dbgsym).@(${suffix})) 85 | 86 | for candidate in "${candidates[@]}"; do 87 | mv "${candidate}" "${root_dir}" 88 | done 89 | done 90 | 91 | - name: Generate Checksums 🪪 92 | if: fromJSON(steps.check.outputs.validTag) 93 | run: | 94 | : Generate Checksums 🪪 95 | if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi 96 | shopt -s extglob 97 | 98 | echo "### Checksums" > ${{ github.workspace }}/CHECKSUMS.txt 99 | for file in ${{ github.workspace }}/@(*.exe|*.deb|*.ddeb|*.pkg|*.tar.xz|*.zip); do 100 | echo " ${file##*/}: $(sha256sum "${file}" | cut -d " " -f 1)" >> ${{ github.workspace }}/CHECKSUMS.txt 101 | done 102 | 103 | - name: Create Release 🛫 104 | if: fromJSON(steps.check.outputs.validTag) 105 | id: create_release 106 | uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 107 | with: 108 | draft: true 109 | prerelease: ${{ fromJSON(steps.check.outputs.prerelease) }} 110 | tag_name: ${{ steps.check.outputs.version }} 111 | name: ${{ needs.build-project.outputs.pluginName }} ${{ steps.check.outputs.version }} 112 | body_path: ${{ github.workspace }}/CHECKSUMS.txt 113 | files: | 114 | ${{ github.workspace }}/*.exe 115 | ${{ github.workspace }}/*.zip 116 | ${{ github.workspace }}/*.pkg 117 | ${{ github.workspace }}/*.deb 118 | ${{ github.workspace }}/*.ddeb 119 | ${{ github.workspace }}/*.tar.xz 120 | -------------------------------------------------------------------------------- /src/parsers/html.cpp: -------------------------------------------------------------------------------- 1 | #include "request-data.h" 2 | #include "plugin-support.h" 3 | #include "errors.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | lxb_inline lxb_status_t serializer_callback(const lxb_char_t *data, size_t len, void *ctx) 14 | { 15 | ((std::string *)ctx)->append((const char *)data, len); 16 | return LXB_STATUS_OK; 17 | } 18 | 19 | lxb_status_t find_callback(lxb_dom_node_t *node, lxb_css_selector_specificity_t spec, void *data) 20 | { 21 | UNUSED_PARAMETER(spec); 22 | std::string str; 23 | lxb_status_t status = lxb_html_serialize_deep_cb(node, serializer_callback, &str); 24 | if (status == LXB_STATUS_OK) { 25 | ((std::vector *)data)->push_back(str); 26 | } 27 | return status; 28 | } 29 | 30 | lxb_status_t find_with_selectors(const std::string &slctrs, lxb_html_document_t *document, 31 | std::vector &found) 32 | { 33 | lxb_css_parser_t *parser = nullptr; 34 | lxb_css_selector_list_t *list = nullptr; 35 | lxb_selectors_t *selectors = nullptr; 36 | lxb_status_t status = LXB_STATUS_ERROR; 37 | 38 | do { 39 | parser = lxb_css_parser_create(); 40 | if (!parser) { 41 | obs_log(LOG_ERROR, "Failed to create CSS parser"); 42 | break; 43 | } 44 | 45 | status = lxb_css_parser_init(parser, nullptr); 46 | if (status != LXB_STATUS_OK) { 47 | obs_log(LOG_ERROR, "Failed to init CSS parser"); 48 | break; 49 | } 50 | 51 | selectors = lxb_selectors_create(); 52 | if (!selectors) { 53 | obs_log(LOG_ERROR, "Failed to create selectors"); 54 | break; 55 | } 56 | 57 | status = lxb_selectors_init(selectors); 58 | if (status != LXB_STATUS_OK) { 59 | obs_log(LOG_ERROR, "Failed to init selectors"); 60 | break; 61 | } 62 | 63 | list = lxb_css_selectors_parse(parser, (const lxb_char_t *)slctrs.c_str(), 64 | slctrs.length()); 65 | if (!list || parser->status != LXB_STATUS_OK) { 66 | obs_log(LOG_ERROR, "Failed to parse CSS selectors"); 67 | break; 68 | } 69 | 70 | lxb_dom_node_t *body = 71 | lxb_dom_interface_node(lxb_html_document_body_element(document)); 72 | if (!body) { 73 | obs_log(LOG_ERROR, "Failed to get document body"); 74 | break; 75 | } 76 | 77 | status = lxb_selectors_find(selectors, body, list, find_callback, &found); 78 | if (status != LXB_STATUS_OK) { 79 | obs_log(LOG_ERROR, "Failed to find nodes by CSS Selectors"); 80 | break; 81 | } 82 | 83 | } while (0); 84 | 85 | // Cleanup 86 | if (list) { 87 | lxb_css_selector_list_destroy_memory(list); 88 | } 89 | if (selectors) { 90 | lxb_selectors_destroy(selectors, true); 91 | } 92 | if (parser) { 93 | lxb_css_parser_destroy(parser, true); 94 | } 95 | 96 | return status; 97 | } 98 | 99 | struct request_data_handler_response parse_html(struct request_data_handler_response response, 100 | const url_source_request_data *request_data) 101 | { 102 | lxb_html_document_t *document = nullptr; 103 | 104 | try { 105 | document = lxb_html_document_create(); 106 | if (!document) { 107 | return make_fail_parse_response("Failed to create HTML document"); 108 | } 109 | 110 | lxb_status_t status = 111 | lxb_html_document_parse(document, (const lxb_char_t *)response.body.c_str(), 112 | response.body.length()); 113 | 114 | if (status != LXB_STATUS_OK) { 115 | lxb_html_document_destroy(document); 116 | return make_fail_parse_response("Failed to parse HTML"); 117 | } 118 | 119 | if (!request_data->output_cssselector.empty()) { 120 | std::vector found; 121 | status = find_with_selectors(request_data->output_cssselector, document, 122 | found); 123 | 124 | if (status != LXB_STATUS_OK) { 125 | lxb_html_document_destroy(document); 126 | return make_fail_parse_response( 127 | "Failed to find element with CSS selector"); 128 | } 129 | 130 | response.body_parts_parsed = std::move(found); 131 | } else { 132 | response.body_parts_parsed.push_back(response.body); 133 | } 134 | 135 | lxb_html_document_destroy(document); 136 | return response; 137 | 138 | } catch (const std::exception &e) { 139 | if (document) { 140 | lxb_html_document_destroy(document); 141 | } 142 | return make_fail_parse_response(std::string("HTML parsing exception: ") + e.what()); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /.github/scripts/build-macos: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | builtin emulate -L zsh 4 | setopt EXTENDED_GLOB 5 | setopt PUSHD_SILENT 6 | setopt ERR_EXIT 7 | setopt ERR_RETURN 8 | setopt NO_UNSET 9 | setopt PIPE_FAIL 10 | setopt NO_AUTO_PUSHD 11 | setopt NO_PUSHD_IGNORE_DUPS 12 | setopt FUNCTION_ARGZERO 13 | 14 | ## Enable for script debugging 15 | # setopt WARN_CREATE_GLOBAL 16 | # setopt WARN_NESTED_VAR 17 | # setopt XTRACE 18 | 19 | if (( ! ${+CI} )) { 20 | print -u2 -PR "%F{1} ✖︎ ${ZSH_ARGZERO:t:r} requires CI environment.%f" 21 | exit 1 22 | } 23 | 24 | autoload -Uz is-at-least && if ! is-at-least 5.9; then 25 | print -u2 -PR "${CI:+::error::}%F{1}${funcstack[1]##*/}:%f Running on Zsh version %B${ZSH_VERSION}%b, but Zsh %B5.2%b is the minimum supported version. Upgrade Zsh to fix this issue." 26 | exit 1 27 | fi 28 | 29 | TRAPZERR() { 30 | print -u2 -PR "::error::%F{1} ✖︎ script execution error%f" 31 | print -PR -e " 32 | Callstack: 33 | ${(j:\n :)funcfiletrace} 34 | " 35 | 36 | exit 2 37 | } 38 | 39 | build() { 40 | if (( ! ${+SCRIPT_HOME} )) typeset -g SCRIPT_HOME=${ZSH_ARGZERO:A:h} 41 | local host_os='macos' 42 | local project_root=${SCRIPT_HOME:A:h:h} 43 | local buildspec_file=${project_root}/buildspec.json 44 | 45 | fpath=("${SCRIPT_HOME}/utils.zsh" ${fpath}) 46 | autoload -Uz log_group log_info log_error log_output check_macos setup_ccache 47 | 48 | if [[ ! -r ${buildspec_file} ]] { 49 | log_error \ 50 | 'No buildspec.json found. Please create a build specification for your project.' 51 | return 2 52 | } 53 | 54 | local -i debug=0 55 | 56 | local config='RelWithDebInfo' 57 | local -r -a _valid_configs=(Debug RelWithDebInfo Release MinSizeRel) 58 | local -i codesign=0 59 | 60 | local -a args 61 | while (( # )) { 62 | case ${1} { 63 | -c|--config) 64 | if (( # == 1 )) || [[ ${2:0:1} == '-' ]] { 65 | log_error "Missing value for option %B${1}%b" 66 | log_output ${_usage} 67 | exit 2 68 | } 69 | ;; 70 | } 71 | case ${1} { 72 | --) shift; args+=($@); break ;; 73 | -c|--config) 74 | if (( ! ${_valid_configs[(Ie)${2}]} )) { 75 | log_error "Invalid value %B${2}%b for option %B${1}%b" 76 | exit 2 77 | } 78 | config=${2} 79 | shift 2 80 | ;; 81 | -s|--codesign) codesign=1; shift ;; 82 | --debug) debug=1; shift ;; 83 | *) log_error "Unknown option: %B${1}%b"; exit 2 ;; 84 | } 85 | } 86 | 87 | set -- ${(@)args} 88 | 89 | check_macos 90 | 91 | local product_name 92 | local product_version 93 | read -r product_name product_version <<< \ 94 | "$(jq -r '. | {name, version} | join(" ")' ${buildspec_file})" 95 | 96 | pushd ${project_root} 97 | 98 | local -a cmake_args=() 99 | local -a cmake_build_args=(--build) 100 | local -a cmake_install_args=(--install) 101 | 102 | if (( debug )) cmake_args+=(--debug-output) 103 | 104 | cmake_args+=(--preset 'macos-ci') 105 | 106 | typeset -gx NSUnbufferedIO=YES 107 | 108 | typeset -gx CODESIGN_IDENT="${CODESIGN_IDENT:--}" 109 | if (( codesign )) && [[ -z ${CODESIGN_TEAM} ]] { 110 | typeset -gx CODESIGN_TEAM="$(print "${CODESIGN_IDENT}" | /usr/bin/sed -En 's/.+\((.+)\)/\1/p')" 111 | } 112 | 113 | log_group "Configuring ${product_name}..." 114 | cmake -S ${project_root} ${cmake_args} 115 | 116 | log_group "Building ${product_name}..." 117 | run_xcodebuild() { 118 | if (( debug )) { 119 | xcodebuild ${@} 120 | } else { 121 | if [[ ${GITHUB_EVENT_NAME} == push ]] { 122 | xcodebuild ${@} 2>&1 | xcbeautify --renderer terminal 123 | } else { 124 | xcodebuild ${@} 2>&1 | xcbeautify --renderer github-actions 125 | } 126 | } 127 | } 128 | 129 | local -a build_args=( 130 | ONLY_ACTIVE_ARCH=NO 131 | -arch arm64 132 | -arch x86_64 133 | -project ${product_name}.xcodeproj 134 | -target ${product_name} 135 | -destination "generic/platform=macOS,name=Any Mac" 136 | -configuration ${config} 137 | -parallelizeTargets 138 | -hideShellScriptEnvironment 139 | build 140 | ) 141 | 142 | pushd build_macos 143 | run_xcodebuild ${build_args} 144 | popd 145 | 146 | log_group "Installing ${product_name}..." 147 | cmake --install build_macos --config ${config} --prefix "${project_root}/release/${config}" 148 | 149 | popd 150 | log_group 151 | } 152 | 153 | build ${@} 154 | -------------------------------------------------------------------------------- /src/obs-source-util.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "obs-source-util.h" 3 | #include "plugin-support.h" 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | void init_source_render_data(source_render_data *tf) 13 | { 14 | tf->texrender = gs_texrender_create(GS_BGRA, GS_ZS_NONE); 15 | tf->stagesurface = nullptr; 16 | } 17 | 18 | void destroy_source_render_data(source_render_data *tf) 19 | { 20 | if (tf->texrender) { 21 | gs_texrender_destroy(tf->texrender); 22 | tf->texrender = nullptr; 23 | } 24 | if (tf->stagesurface) { 25 | gs_stagesurface_destroy(tf->stagesurface); 26 | tf->stagesurface = nullptr; 27 | } 28 | } 29 | 30 | /** 31 | * @brief Get RGBA from the stage surface 32 | * 33 | * @param tf The filter data 34 | * @param width The width of the stage surface (output) 35 | * @param height The height of the stage surface (output) 36 | * @param scale Scale the output by this factor 37 | * @return The RGBA buffer (4 bytes per pixel) or an empty vector if there was an error 38 | */ 39 | std::vector get_rgba_from_source_render(obs_source_t *source, source_render_data *tf, 40 | uint32_t &width, uint32_t &height, float scale) 41 | { 42 | if (!obs_source_enabled(source)) { 43 | obs_log(LOG_ERROR, "Source is not enabled"); 44 | return std::vector(); 45 | } 46 | 47 | width = obs_source_get_base_width(source); 48 | height = obs_source_get_base_height(source); 49 | if (width == 0 || height == 0) { 50 | obs_log(LOG_ERROR, "Width or height is 0"); 51 | return std::vector(); 52 | } 53 | // scale the width and height 54 | width = (uint32_t)((float)width * scale); 55 | height = (uint32_t)((float)height * scale); 56 | 57 | // enter graphics context 58 | obs_enter_graphics(); 59 | 60 | gs_texrender_reset(tf->texrender); 61 | if (!gs_texrender_begin(tf->texrender, width, height)) { 62 | obs_log(LOG_ERROR, "Could not begin texrender"); 63 | return std::vector(); 64 | } 65 | struct vec4 background; 66 | vec4_zero(&background); 67 | gs_clear(GS_CLEAR_COLOR, &background, 0.0f, 0); 68 | gs_ortho(0.0f, static_cast(width), 0.0f, static_cast(height), -100.0f, 69 | 100.0f); 70 | gs_blend_state_push(); 71 | gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); 72 | obs_source_video_render(source); 73 | gs_blend_state_pop(); 74 | gs_texrender_end(tf->texrender); 75 | 76 | if (tf->stagesurface) { 77 | uint32_t stagesurf_width = gs_stagesurface_get_width(tf->stagesurface); 78 | uint32_t stagesurf_height = gs_stagesurface_get_height(tf->stagesurface); 79 | if (stagesurf_width != width || stagesurf_height != height) { 80 | gs_stagesurface_destroy(tf->stagesurface); 81 | tf->stagesurface = nullptr; 82 | } 83 | } 84 | if (!tf->stagesurface) { 85 | tf->stagesurface = gs_stagesurface_create(width, height, GS_BGRA); 86 | } 87 | gs_stage_texture(tf->stagesurface, gs_texrender_get_texture(tf->texrender)); 88 | uint8_t *video_data; 89 | uint32_t linesize; 90 | if (!gs_stagesurface_map(tf->stagesurface, &video_data, &linesize)) { 91 | obs_log(LOG_ERROR, "Cannot map stage surface"); 92 | return std::vector(); 93 | } 94 | obs_log(LOG_INFO, "linesize: %d, width: %d, height: %d", linesize, width, height); 95 | if (linesize != width * 4) { 96 | obs_log(LOG_WARNING, "linesize %d != width %d * 4", linesize, width); 97 | } 98 | std::vector rgba(width * height * 4); 99 | for (uint32_t i = 0; i < height; i++) { 100 | memcpy(rgba.data() + i * width * 4, video_data + i * linesize, width * 4); 101 | } 102 | 103 | gs_stagesurface_unmap(tf->stagesurface); 104 | 105 | // leave graphics context 106 | obs_leave_graphics(); 107 | 108 | return rgba; 109 | } 110 | 111 | std::string convert_rgba_buffer_to_png_base64(const std::vector &rgba, uint32_t width, 112 | uint32_t height) 113 | { 114 | // use Qt to convert the RGBA buffer to an encoded image buffer 115 | QImage image(rgba.data(), width, height, QImage::Format_RGBA8888); 116 | QByteArray ba; 117 | QBuffer buffer(&ba); 118 | buffer.open(QIODevice::WriteOnly); 119 | image.save(&buffer, "PNG"); 120 | buffer.close(); 121 | 122 | // convert the encoded image buffer to a base64 string 123 | QByteArray base64 = ba.toBase64(); 124 | std::string base64_str = base64.toStdString(); 125 | 126 | // json string escape 127 | nlohmann::json j(base64_str); 128 | std::string escaped = j.dump(); 129 | // remove the quotes 130 | escaped = escaped.substr(1, escaped.size() - 2); 131 | 132 | return escaped; 133 | } 134 | 135 | std::string get_source_name_without_prefix(const std::string &source_name) 136 | { 137 | if (source_name.size() > 0 && source_name[0] == '(') { 138 | size_t end = source_name.find(')'); 139 | if (end != std::string::npos && end + 2 < source_name.size()) { 140 | return source_name.substr(end + 2); 141 | } 142 | } 143 | return source_name; 144 | } 145 | -------------------------------------------------------------------------------- /cmake/windows/helpers.cmake: -------------------------------------------------------------------------------- 1 | # CMake Windows helper functions module 2 | 3 | # cmake-format: off 4 | # cmake-lint: disable=C0103 5 | # cmake-format: on 6 | 7 | include_guard(GLOBAL) 8 | 9 | include(helpers_common) 10 | 11 | # set_target_properties_plugin: Set target properties for use in obs-studio 12 | function(set_target_properties_plugin target) 13 | set(options "") 14 | set(oneValueArgs "") 15 | set(multiValueArgs PROPERTIES) 16 | cmake_parse_arguments(PARSE_ARGV 0 _STPO "${options}" "${oneValueArgs}" "${multiValueArgs}") 17 | 18 | message(DEBUG "Setting additional properties for target ${target}...") 19 | 20 | while(_STPO_PROPERTIES) 21 | list(POP_FRONT _STPO_PROPERTIES key value) 22 | set_property(TARGET ${target} PROPERTY ${key} "${value}") 23 | endwhile() 24 | 25 | string(TIMESTAMP CURRENT_YEAR "%Y") 26 | 27 | set_target_properties(${target} PROPERTIES VERSION 0 SOVERSION ${PLUGIN_VERSION}) 28 | 29 | install(TARGETS ${target} RUNTIME DESTINATION bin/64bit LIBRARY DESTINATION obs-plugins/64bit) 30 | 31 | install( 32 | FILES "$" 33 | CONFIGURATIONS RelWithDebInfo Debug Release 34 | DESTINATION obs-plugins/64bit 35 | OPTIONAL 36 | ) 37 | 38 | if(OBS_BUILD_DIR) 39 | add_custom_command( 40 | TARGET ${target} 41 | POST_BUILD 42 | COMMAND "${CMAKE_COMMAND}" -E make_directory "${OBS_BUILD_DIR}/obs-plugins/64bit" 43 | COMMAND 44 | "${CMAKE_COMMAND}" -E copy_if_different "$" 45 | "$<$:$>" "${OBS_BUILD_DIR}/obs-plugins/64bit" 46 | COMMENT "Copy ${target} to obs-studio directory ${OBS_BUILD_DIR}" 47 | VERBATIM 48 | ) 49 | endif() 50 | 51 | if(TARGET plugin-support) 52 | target_link_libraries(${target} PRIVATE plugin-support) 53 | endif() 54 | 55 | target_install_resources(${target}) 56 | 57 | get_target_property(target_sources ${target} SOURCES) 58 | set(target_ui_files ${target_sources}) 59 | list(FILTER target_ui_files INCLUDE REGEX ".+\\.(ui|qrc)") 60 | source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "UI Files" FILES ${target_ui_files}) 61 | 62 | set(valid_uuid FALSE) 63 | check_uuid(${_windowsAppUUID} valid_uuid) 64 | if(NOT valid_uuid) 65 | message(FATAL_ERROR "Specified Windows package UUID is not a valid UUID value: ${_windowsAppUUID}") 66 | else() 67 | set(UUID_APP ${_windowsAppUUID}) 68 | endif() 69 | 70 | configure_file( 71 | cmake/windows/resources/installer-Windows.iss.in 72 | "${CMAKE_CURRENT_BINARY_DIR}/installer-Windows.generated.iss" 73 | ) 74 | 75 | configure_file(cmake/windows/resources/resource.rc.in "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.rc") 76 | target_sources(${CMAKE_PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.rc") 77 | endfunction() 78 | 79 | # Helper function to add resources into bundle 80 | function(target_install_resources target) 81 | message(DEBUG "Installing resources for target ${target}...") 82 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/data") 83 | file(GLOB_RECURSE data_files "${CMAKE_CURRENT_SOURCE_DIR}/data/*") 84 | foreach(data_file IN LISTS data_files) 85 | cmake_path( 86 | RELATIVE_PATH 87 | data_file 88 | BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" 89 | OUTPUT_VARIABLE relative_path 90 | ) 91 | cmake_path(GET relative_path PARENT_PATH relative_path) 92 | target_sources(${target} PRIVATE "${data_file}") 93 | source_group("Resources/${relative_path}" FILES "${data_file}") 94 | endforeach() 95 | 96 | install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" DESTINATION data/obs-plugins/${target} USE_SOURCE_PERMISSIONS) 97 | 98 | if(OBS_BUILD_DIR) 99 | add_custom_command( 100 | TARGET ${target} 101 | POST_BUILD 102 | COMMAND "${CMAKE_COMMAND}" -E make_directory "${OBS_BUILD_DIR}/data/obs-plugins/${target}" 103 | COMMAND 104 | "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/data" 105 | "${OBS_BUILD_DIR}/data/obs-plugins/${target}" 106 | COMMENT "Copy ${target} resources to data directory" 107 | VERBATIM 108 | ) 109 | endif() 110 | endif() 111 | endfunction() 112 | 113 | # Helper function to add a specific resource to a bundle 114 | function(target_add_resource target resource) 115 | message(DEBUG "Add resource '${resource}' to target ${target} at destination '${target_destination}'...") 116 | 117 | install(FILES "${resource}" DESTINATION data/obs-plugins/${target} COMPONENT Runtime) 118 | 119 | if(OBS_BUILD_DIR) 120 | add_custom_command( 121 | TARGET ${target} 122 | POST_BUILD 123 | COMMAND "${CMAKE_COMMAND}" -E make_directory "${OBS_BUILD_DIR}/data/obs-plugins/${target}" 124 | COMMAND "${CMAKE_COMMAND}" -E copy "${resource}" "${OBS_BUILD_DIR}/data/obs-plugins/${target}" 125 | COMMENT "Copy ${target} resource ${resource} to library directory" 126 | VERBATIM 127 | ) 128 | endif() 129 | source_group("Resources" FILES "${resource}") 130 | endfunction() 131 | -------------------------------------------------------------------------------- /src/websocket-client.cpp: -------------------------------------------------------------------------------- 1 | 2 | #pragma warning(disable : 4267) 3 | 4 | #define ASIO_STANDALONE 5 | #define _WEBSOCKETPP_CPP11_TYPE_TRAITS_ 6 | #define _WEBSOCKETPP_CPP11_RANDOM_DEVICE_ 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | 20 | #include "request-data.h" 21 | #include "websocket-client.h" 22 | #include "plugin-support.h" 23 | 24 | #include 25 | 26 | typedef websocketpp::client ws_client; 27 | 28 | struct WebSocketClientWrapper { 29 | ws_client client; 30 | websocketpp::connection_hdl connection; 31 | std::unique_ptr asio_thread; 32 | std::string last_received_message; 33 | std::atomic is_connected{false}; 34 | std::mutex mutex; 35 | std::condition_variable cv; 36 | 37 | WebSocketClientWrapper() 38 | { 39 | try { 40 | client.clear_access_channels(websocketpp::log::alevel::all); 41 | client.clear_error_channels(websocketpp::log::elevel::all); 42 | 43 | client.init_asio(); 44 | client.start_perpetual(); 45 | 46 | client.set_message_handler(std::bind(&WebSocketClientWrapper::on_message, 47 | this, std::placeholders::_1, 48 | std::placeholders::_2)); 49 | 50 | client.set_open_handler(std::bind(&WebSocketClientWrapper::on_open, this, 51 | std::placeholders::_1)); 52 | 53 | client.set_close_handler(std::bind(&WebSocketClientWrapper::on_close, this, 54 | std::placeholders::_1)); 55 | 56 | asio_thread = std::make_unique(&ws_client::run, &client); 57 | } catch (const std::exception &e) { 58 | // Log the error or handle it appropriately 59 | throw std::runtime_error("Failed to initialize WebSocket client: " + 60 | std::string(e.what())); 61 | } 62 | } 63 | 64 | ~WebSocketClientWrapper() 65 | { 66 | if (is_connected.load()) { 67 | close(); 68 | } 69 | client.stop_perpetual(); 70 | if (asio_thread && asio_thread->joinable()) { 71 | asio_thread->join(); 72 | } 73 | } 74 | 75 | void on_message(websocketpp::connection_hdl, ws_client::message_ptr msg) 76 | { 77 | std::lock_guard lock(mutex); 78 | last_received_message = msg->get_payload(); 79 | cv.notify_one(); 80 | } 81 | 82 | void on_open(websocketpp::connection_hdl hdl) 83 | { 84 | connection = hdl; 85 | is_connected.store(true); 86 | cv.notify_one(); 87 | } 88 | 89 | void on_close(websocketpp::connection_hdl) 90 | { 91 | is_connected.store(false); 92 | cv.notify_one(); 93 | } 94 | 95 | bool connect(const std::string &uri) 96 | { 97 | websocketpp::lib::error_code ec; 98 | ws_client::connection_ptr con = client.get_connection(uri, ec); 99 | if (ec) { 100 | return false; 101 | } 102 | 103 | client.connect(con); 104 | 105 | std::unique_lock lock(mutex); 106 | return cv.wait_for(lock, std::chrono::seconds(5), 107 | [this] { return is_connected.load(); }); 108 | } 109 | 110 | bool send(const std::string &message) 111 | { 112 | if (!is_connected.load()) { 113 | return false; 114 | } 115 | 116 | websocketpp::lib::error_code ec; 117 | client.send(connection, message, websocketpp::frame::opcode::text, ec); 118 | return !ec; 119 | } 120 | 121 | bool receive(std::string &message, std::chrono::milliseconds timeout) 122 | { 123 | std::unique_lock lock(mutex); 124 | if (cv.wait_for(lock, timeout, [this] { return !last_received_message.empty(); })) { 125 | message = std::move(last_received_message); 126 | last_received_message.clear(); 127 | return true; 128 | } 129 | return false; 130 | } 131 | 132 | void close() 133 | { 134 | if (is_connected.load()) { 135 | websocketpp::lib::error_code ec; 136 | client.close(connection, websocketpp::close::status::normal, 137 | "Closing connection", ec); 138 | if (ec) { 139 | // Handle error 140 | obs_log(LOG_WARNING, "Failed to close WebSocket connection: %s", 141 | ec.message().c_str()); 142 | } 143 | } 144 | } 145 | }; 146 | 147 | struct request_data_handler_response 148 | websocket_request_handler(url_source_request_data *request_data) 149 | { 150 | request_data_handler_response response; 151 | 152 | try { 153 | if (!request_data->ws_client_wrapper) { 154 | request_data->ws_client_wrapper = new WebSocketClientWrapper(); 155 | } 156 | 157 | if (!request_data->ws_connected) { 158 | if (!request_data->ws_client_wrapper->connect(request_data->url)) { 159 | throw std::runtime_error("Could not create WebSocket connection"); 160 | } 161 | request_data->ws_connected = true; 162 | } 163 | 164 | nlohmann::json json; // json object or variables for inja 165 | inja::Environment env; 166 | prepare_inja_env(&env, request_data, response, json); 167 | 168 | if (response.status_code != URL_SOURCE_REQUEST_SUCCESS) { 169 | return response; 170 | } 171 | 172 | std::string message = env.render(request_data->body, json); 173 | 174 | if (!request_data->ws_client_wrapper->send(message)) { 175 | throw std::runtime_error("Failed to send WebSocket message"); 176 | } 177 | 178 | std::string received_message; 179 | if (request_data->ws_client_wrapper->receive(received_message, 180 | std::chrono::milliseconds(5000))) { 181 | response.body = std::move(received_message); 182 | response.status_code = URL_SOURCE_REQUEST_SUCCESS; 183 | } else { 184 | throw std::runtime_error("Timeout waiting for WebSocket response"); 185 | } 186 | } catch (const std::exception &e) { 187 | response.status_code = URL_SOURCE_REQUEST_STANDARD_ERROR_CODE; 188 | response.error_message = 189 | "Error handling WebSocket request: " + std::string(e.what()); 190 | } 191 | 192 | return response; 193 | } 194 | -------------------------------------------------------------------------------- /.github/scripts/package-macos: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | builtin emulate -L zsh 4 | setopt EXTENDED_GLOB 5 | setopt PUSHD_SILENT 6 | setopt ERR_EXIT 7 | setopt ERR_RETURN 8 | setopt NO_UNSET 9 | setopt PIPE_FAIL 10 | setopt NO_AUTO_PUSHD 11 | setopt NO_PUSHD_IGNORE_DUPS 12 | setopt FUNCTION_ARGZERO 13 | 14 | ## Enable for script debugging 15 | # setopt WARN_CREATE_GLOBAL 16 | # setopt WARN_NESTED_VAR 17 | # setopt XTRACE 18 | 19 | if (( ! ${+CI} )) { 20 | print -u2 -PR "%F{1} ✖︎ ${ZSH_ARGZERO:t:r} requires CI environment%f" 21 | exit 1 22 | } 23 | 24 | autoload -Uz is-at-least && if ! is-at-least 5.9; then 25 | print -u2 -PR "${CI:+::error::}%F{1}${funcstack[1]##*/}:%f Running on Zsh version %B${ZSH_VERSION}%b, but Zsh %B5.2%b is the minimum supported version. Upgrade Zsh to fix this issue." 26 | exit 1 27 | fi 28 | 29 | TRAPZERR() { 30 | print -u2 -PR "::error::%F{1} ✖︎ script execution error%f" 31 | print -PR -e " 32 | Callstack: 33 | ${(j:\n :)funcfiletrace} 34 | " 35 | 36 | exit 2 37 | } 38 | 39 | package() { 40 | if (( ! ${+SCRIPT_HOME} )) typeset -g SCRIPT_HOME=${ZSH_ARGZERO:A:h} 41 | local host_os='macos' 42 | local project_root=${SCRIPT_HOME:A:h:h} 43 | local buildspec_file=${project_root}/buildspec.json 44 | 45 | fpath=("${SCRIPT_HOME}/utils.zsh" ${fpath}) 46 | autoload -Uz log_group log_error log_output check_macos 47 | 48 | if [[ ! -r ${buildspec_file} ]] { 49 | log_error \ 50 | 'No buildspec.json found. Please create a build specification for your project.' 51 | return 2 52 | } 53 | 54 | local -i debug=0 55 | 56 | local config='RelWithDebInfo' 57 | local -r -a _valid_configs=(Debug RelWithDebInfo Release MinSizeRel) 58 | 59 | local -i codesign=0 60 | local -i notarize=0 61 | local -i package=0 62 | 63 | local -a args 64 | while (( # )) { 65 | case ${1} { 66 | -c|--config) 67 | if (( # == 1 )) || [[ ${2:0:1} == '-' ]] { 68 | log_error "Missing value for option %B${1}%b" 69 | exit 2 70 | } 71 | ;; 72 | } 73 | case ${1} { 74 | --) shift; args+=($@); break ;; 75 | -c|--config) 76 | if (( !${_valid_configs[(Ie)${2}]} )) { 77 | log_error "Invalid value %B${2}%b for option %B${1}%b" 78 | exit 2 79 | } 80 | config=${2} 81 | shift 2 82 | ;; 83 | -s|--codesign) typeset -g codesign=1; shift ;; 84 | -n|--notarize) typeset -g notarize=1; typeset -g codesign=1; shift ;; 85 | -p|--package) typeset -g package=1; shift ;; 86 | --debug) debug=1; shift ;; 87 | *) log_error "Unknown option: %B${1}%b"; exit 2 ;; 88 | } 89 | } 90 | 91 | set -- ${(@)args} 92 | 93 | check_macos 94 | 95 | local product_name 96 | local product_version 97 | read -r product_name product_version <<< \ 98 | "$(jq -r '. | {name, version} | join(" ")' ${buildspec_file})" 99 | 100 | local output_name="${product_name}-${product_version}-${host_os}-universal" 101 | 102 | if [[ ! -d ${project_root}/release/${config}/${product_name}.plugin ]] { 103 | log_error 'No release artifact found. Run the build script or the CMake install procedure first.' 104 | return 2 105 | } 106 | 107 | if (( package )) { 108 | if [[ ! -f ${project_root}/release/${config}/${product_name}.pkg ]] { 109 | log_error 'Installer Package not found. Run the build script or the CMake build and install procedures first.' 110 | return 2 111 | } 112 | 113 | log_group "Packaging ${product_name}..." 114 | pushd ${project_root} 115 | 116 | typeset -gx CODESIGN_IDENT="${CODESIGN_IDENT:--}" 117 | typeset -gx CODESIGN_IDENT_INSTALLER="${CODESIGN_IDENT_INSTALLER:--}" 118 | typeset -gx CODESIGN_TEAM="$(print "${CODESIGN_IDENT}" | /usr/bin/sed -En 's/.+\((.+)\)/\1/p')" 119 | 120 | if (( codesign )) { 121 | productsign \ 122 | --sign "${CODESIGN_IDENT_INSTALLER}" \ 123 | ${project_root}/release/${config}/${product_name}.pkg \ 124 | ${project_root}/release/${output_name}.pkg 125 | 126 | rm ${project_root}/release/${config}/${product_name}.pkg 127 | } else { 128 | mv ${project_root}/release/${config}/${product_name}.pkg \ 129 | ${project_root}/release/${output_name}.pkg 130 | } 131 | 132 | if (( codesign && notarize )) { 133 | if ! [[ ${CODESIGN_IDENT} != '-' && ${CODESIGN_TEAM} && {CODESIGN_IDENT_USER} && ${CODESIGN_IDENT_PASS} ]] { 134 | log_error "Notarization requires Apple ID and application password." 135 | return 2 136 | } 137 | 138 | if [[ ! -f ${project_root}/release/${output_name}.pkg ]] { 139 | log_error "No package for notarization found." 140 | return 2 141 | } 142 | 143 | xcrun notarytool store-credentials "${product_name}-Codesign-Password" --apple-id "${CODESIGN_IDENT_USER}" --team-id "${CODESIGN_TEAM}" --password "${CODESIGN_IDENT_PASS}" 144 | xcrun notarytool submit ${project_root}/release/${output_name}.pkg --keychain-profile "${product_name}-Codesign-Password" --wait 145 | 146 | local -i _status=0 147 | 148 | xcrun stapler staple ${project_root}/release/${output_name}.pkg || _status=1 149 | 150 | if (( _status )) { 151 | log_error "Notarization failed. Use 'xcrun notarytool log ' to check for errors." 152 | return 2 153 | } 154 | } 155 | popd 156 | } else { 157 | log_group "Archiving ${product_name}..." 158 | pushd ${project_root}/release/${config} 159 | XZ_OPT=-T0 tar -cvJf ${project_root}/release/${output_name}.tar.xz ${product_name}.plugin 160 | popd 161 | } 162 | 163 | if [[ ${config} == Release ]] { 164 | log_group "Archiving ${product_name} Debug Symbols..." 165 | pushd ${project_root}/release/${config} 166 | XZ_OPT=-T0 tar -cvJf ${project_root}/release/${output_name}-dSYMs.tar.xz ${product_name}.plugin.dSYM 167 | popd 168 | } 169 | 170 | log_group 171 | } 172 | 173 | package ${@} 174 | -------------------------------------------------------------------------------- /.github/scripts/package-ubuntu: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | builtin emulate -L zsh 4 | setopt EXTENDED_GLOB 5 | setopt PUSHD_SILENT 6 | setopt ERR_EXIT 7 | setopt ERR_RETURN 8 | setopt NO_UNSET 9 | setopt PIPE_FAIL 10 | setopt NO_AUTO_PUSHD 11 | setopt NO_PUSHD_IGNORE_DUPS 12 | setopt FUNCTION_ARGZERO 13 | 14 | ## Enable for script debugging 15 | # setopt WARN_CREATE_GLOBAL 16 | # setopt WARN_NESTED_VAR 17 | # setopt XTRACE 18 | 19 | autoload -Uz is-at-least && if ! is-at-least 5.2; then 20 | print -u2 -PR "${CI:+::error::}%F{1}${funcstack[1]##*/}:%f Running on Zsh version %B${ZSH_VERSION}%b, but Zsh %B5.2%b is the minimum supported version. Upgrade Zsh to fix this issue." 21 | exit 1 22 | fi 23 | 24 | TRAPZERR() { 25 | if (( ${_loglevel:-3} > 2 )) { 26 | print -u2 -PR "${CI:+::error::}%F{1} ✖︎ script execution error%f" 27 | print -PR -e " 28 | Callstack: 29 | ${(j:\n :)funcfiletrace} 30 | " 31 | } 32 | 33 | exit 2 34 | } 35 | 36 | package() { 37 | if (( ! ${+SCRIPT_HOME} )) typeset -g SCRIPT_HOME=${ZSH_ARGZERO:A:h} 38 | local host_os='ubuntu' 39 | local project_root=${SCRIPT_HOME:A:h:h} 40 | local buildspec_file=${project_root}/buildspec.json 41 | 42 | fpath=("${SCRIPT_HOME}/utils.zsh" ${fpath}) 43 | autoload -Uz set_loglevel log_info log_group log_error log_output check_${host_os} 44 | 45 | if [[ ! -r ${buildspec_file} ]] { 46 | log_error \ 47 | 'No buildspec.json found. Please create a build specification for your project.' 48 | return 2 49 | } 50 | 51 | local -i debug=0 52 | local -i verbosity=1 53 | local -r _version='2.0.0' 54 | local -r -a _valid_targets=( 55 | ubuntu-x86_64 56 | ) 57 | local target 58 | local config='RelWithDebInfo' 59 | local -r -a _valid_configs=(Debug RelWithDebInfo Release MinSizeRel) 60 | local -i codesign=0 61 | local -i notarize=0 62 | local -i package=0 63 | local -i skip_deps=0 64 | 65 | local -r _usage=" 66 | Usage: %B${functrace[1]%:*}%b