├── .github ├── scripts │ ├── utils.zsh │ │ ├── log_output │ │ ├── log_info │ │ ├── log_status │ │ ├── log_warning │ │ ├── log_error │ │ ├── mkcd │ │ ├── log_debug │ │ ├── log_group │ │ ├── set_loglevel │ │ ├── check_macos │ │ ├── setup_ubuntu │ │ └── check_ubuntu │ ├── .Brewfile │ ├── .Aptfile │ ├── utils.pwsh │ │ ├── Ensure-Location.ps1 │ │ ├── Invoke-External.ps1 │ │ └── Logger.ps1 │ ├── Build-Windows.ps1 │ ├── build-macos │ ├── Package-Windows.ps1 │ ├── package-macos │ ├── package-ubuntu │ └── build-ubuntu ├── FUNDING.yml ├── workflows │ ├── dispatch.yaml │ ├── check-format.yaml │ ├── pr-pull.yaml │ ├── push.yaml │ └── build-project.yaml └── actions │ ├── run-gersemi │ └── action.yaml │ ├── run-clang-format │ └── action.yaml │ ├── check-changes │ └── action.yaml │ ├── build-plugin │ └── action.yaml │ ├── package-plugin │ └── action.yaml │ └── setup-macos-codesigning │ └── action.yaml ├── README.md ├── .gersemirc ├── src ├── components │ ├── ClickableLabel.hpp │ ├── SliderIgnorewheel.hpp │ ├── AbsoluteSlider.hpp │ ├── SliderIgnorewheel.cpp │ ├── SceneTree.hpp │ ├── MediaControls.hpp │ ├── AbsoluteSlider.cpp │ └── SceneTree.cpp ├── dialogs │ ├── MediaEdit.hpp │ └── MediaEdit.cpp ├── models │ ├── MediaData.hpp │ └── MediaData.cpp ├── plugin-support.h ├── plugin-support.c.in ├── Soundboard.hpp └── forms │ ├── MediaEdit.ui │ ├── Soundboard.ui │ └── MediaControls.ui ├── .gitignore ├── cmake ├── windows │ ├── defaults.cmake │ ├── buildspec.cmake │ ├── resources │ │ ├── resource.rc.in │ │ └── installer-Windows.iss.in │ ├── compilerconfig.cmake │ └── helpers.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 │ └── xcode.cmake ├── common │ ├── osconfig.cmake │ ├── ccache.cmake │ ├── buildnumber.cmake │ ├── helpers_common.cmake │ ├── compiler_common.cmake │ ├── bootstrap.cmake │ └── buildspec_common.cmake └── linux │ ├── compilerconfig.cmake │ ├── defaults.cmake │ └── helpers.cmake ├── data └── locale │ └── en-US.ini ├── CMakeLists.txt ├── CMakePresets.json └── .clang-format /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OBS Soundboard 2 | 3 | OBS plugin that adds a soundboard dock. 4 | -------------------------------------------------------------------------------- /.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/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: cg2121 2 | patreon: cg2121 3 | custom: "https://www.paypal.me/claytong2121" 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gersemirc: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/BlankSpruce/gersemi/master/gersemi/configuration.schema.json 2 | 3 | color: false 4 | definitions: [] 5 | line_length: 120 6 | indent: 2 7 | list_expansion: favour-inlining 8 | quiet: false 9 | unsafe: false 10 | workers: 10 11 | warn_about_unknown_commands: false 12 | -------------------------------------------------------------------------------- /src/components/ClickableLabel.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class ClickableLabel : public QLabel { 7 | Q_OBJECT 8 | 9 | public: 10 | inline ClickableLabel(QWidget *parent = 0) : QLabel(parent) {} 11 | 12 | signals: 13 | void clicked(); 14 | 15 | protected: 16 | void mousePressEvent(QMouseEvent *event) 17 | { 18 | emit clicked(); 19 | event->accept(); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Exclude everything 2 | /* 3 | 4 | # Except for default project files 5 | !/.github 6 | !/build-aux 7 | !/cmake 8 | !/data 9 | !/src 10 | !.clang-format 11 | !.gersemirc 12 | !.gitignore 13 | !buildspec.json 14 | !CMakeLists.txt 15 | !CMakePresets.json 16 | !LICENSE 17 | !README.md 18 | 19 | # Exclude lock files 20 | *.lock.json 21 | 22 | # Exclude macOS legacy resource forks 23 | .DS_Store 24 | 25 | # Exclude CMake build number cache 26 | /cmake/.CMakeBuildNumber 27 | -------------------------------------------------------------------------------- /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 | 10 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 11 | set( 12 | CMAKE_INSTALL_PREFIX 13 | "$ENV{ALLUSERSPROFILE}/obs-studio/plugins" 14 | CACHE STRING 15 | "Default plugin installation directory" 16 | FORCE 17 | ) 18 | endif() 19 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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/dialogs/MediaEdit.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class QAbstractButton; 7 | class Ui_MediaEdit; 8 | 9 | class MediaEdit : public QDialog { 10 | Q_OBJECT 11 | 12 | private: 13 | QString origText; 14 | std::unique_ptr ui; 15 | 16 | private slots: 17 | void on_browseButton_clicked(); 18 | void on_buttonBox_clicked(QAbstractButton *button); 19 | 20 | public: 21 | MediaEdit(QWidget *parent = nullptr); 22 | ~MediaEdit(); 23 | 24 | void setName(const QString &name); 25 | QString getName(); 26 | 27 | void setPath(const QString &path); 28 | QString getPath(); 29 | 30 | void setLoopChecked(bool checked); 31 | bool loopChecked(); 32 | }; 33 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/check_macos: -------------------------------------------------------------------------------- 1 | autoload -Uz is-at-least log_group log_info log_error log_status 2 | 3 | local macos_version=$(sw_vers -productVersion) 4 | 5 | log_group 'Install macOS build requirements' 6 | log_info 'Checking macOS version...' 7 | if ! is-at-least 11.0 ${macos_version}; then 8 | log_error "Minimum required macOS version is 11.0, but running on macOS ${macos_version}" 9 | return 2 10 | else 11 | log_status "macOS ${macos_version} is recent" 12 | fi 13 | 14 | log_info 'Checking for Homebrew...' 15 | if (( ! ${+commands[brew]} )) { 16 | log_error 'No Homebrew command found. Please install Homebrew (https://brew.sh)' 17 | return 2 18 | } 19 | 20 | brew bundle --file ${SCRIPT_HOME}/.Brewfile 21 | rehash 22 | log_group 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 | steps: 8 | - uses: actions/checkout@v4 9 | with: 10 | fetch-depth: 0 11 | - name: clang-format check 🐉 12 | id: clang-format 13 | uses: ./.github/actions/run-clang-format 14 | with: 15 | failCondition: error 16 | 17 | gersemi: 18 | runs-on: ubuntu-24.04 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | - name: gersemi Check 🎛️ 24 | id: gersemi 25 | uses: ./.github/actions/run-gersemi 26 | with: 27 | failCondition: error 28 | -------------------------------------------------------------------------------- /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_DIR='${CMAKE_SOURCE_DIR}/.ccache' 8 | export CCACHE_MAXSIZE='1G' 9 | export CCACHE_CPP2=true 10 | export CCACHE_DEPEND=true 11 | export CCACHE_DIRECT=true 12 | export CCACHE_FILECLONE=true 13 | export CCACHE_INODECACHE=true 14 | export CCACHE_COMPILERCHECK='content' 15 | 16 | CCACHE_SLOPPINESS='file_stat_matches,include_file_mtime,include_file_ctime,system_headers' 17 | 18 | if [[ "${CMAKE_C_COMPILER_ID}" == "AppleClang" ]]; then 19 | CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS},modules,clang_index_store" 20 | fi 21 | export CCACHE_SLOPPINESS 22 | 23 | if [[ "${CI}" ]]; then 24 | export CCACHE_NOHASHDIR=true 25 | fi 26 | exec "${CMAKE_C_COMPILER_LAUNCHER}" "${CMAKE_C_COMPILER}" "$@" 27 | -------------------------------------------------------------------------------- /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_DIR='${CMAKE_SOURCE_DIR}/.ccache' 8 | export CCACHE_MAXSIZE='1G' 9 | export CCACHE_CPP2=true 10 | export CCACHE_DEPEND=true 11 | export CCACHE_DIRECT=true 12 | export CCACHE_FILECLONE=true 13 | export CCACHE_INODECACHE=true 14 | export CCACHE_COMPILERCHECK='content' 15 | 16 | CCACHE_SLOPPINESS='file_stat_matches,include_file_mtime,include_file_ctime,system_headers' 17 | 18 | if [[ "${CMAKE_C_COMPILER_ID}" == "AppleClang" ]]; then 19 | CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS},modules,clang_index_store" 20 | fi 21 | export CCACHE_SLOPPINESS 22 | 23 | if [[ "${CI}" ]]; then 24 | export CCACHE_NOHASHDIR=true 25 | fi 26 | exec "${CMAKE_CXX_COMPILER_LAUNCHER}" "${CMAKE_CXX_COMPILER}" "$@" 27 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /cmake/common/osconfig.cmake: -------------------------------------------------------------------------------- 1 | # CMake operating system bootstrap module 2 | 3 | include_guard(GLOBAL) 4 | 5 | if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") 6 | set(CMAKE_C_EXTENSIONS FALSE) 7 | set(CMAKE_CXX_EXTENSIONS FALSE) 8 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/windows") 9 | set(OS_WINDOWS TRUE) 10 | elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin") 11 | set(CMAKE_C_EXTENSIONS FALSE) 12 | set(CMAKE_CXX_EXTENSIONS FALSE) 13 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos") 14 | set(OS_MACOS TRUE) 15 | elseif(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux|FreeBSD|OpenBSD") 16 | set(CMAKE_CXX_EXTENSIONS FALSE) 17 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/linux") 18 | string(TOUPPER "${CMAKE_HOST_SYSTEM_NAME}" _SYSTEM_NAME_U) 19 | set(OS_${_SYSTEM_NAME_U} TRUE) 20 | endif() 21 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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_VS_PLATFORM_NAME}) 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 | -------------------------------------------------------------------------------- /data/locale/en-US.ini: -------------------------------------------------------------------------------- 1 | Soundboard="Soundboard" 2 | Description="OBS plugin that adds a soundboard dock." 3 | Sound="Sound" 4 | OpenAudioFile="Open Audio File" 5 | NoFile.Title="No File Selected" 6 | NoFile.Text="Please select an audio file." 7 | SoundHotkey="Soundboard: '%1'" 8 | AddSound="Add Sound" 9 | EditSound="Edit Sound" 10 | StopSound="Stop Sound" 11 | RemoveSound="Remove Sound" 12 | FileNotFound.Title="File not Found" 13 | FileNotFound.Text="The sound file could not be found." 14 | Edit="Edit" 15 | PlaySound="Play Sound" 16 | PauseSound="Pause Sound" 17 | StopSound="Stop Sound" 18 | RestartSound="Restart Sound" 19 | StopHotkey="Soundboard: Stop" 20 | RestartHotkey="Soundboard: Restart" 21 | PlayHotkey="Soundboard: Play" 22 | PauseHotkey="Soundboard: Pause" 23 | MuteHotkey="Soundboard: Mute" 24 | UnmuteHotkey="Soundboard: Unmute" 25 | Name="Name" 26 | File="File" 27 | MediaProps="Sound Properties" 28 | -------------------------------------------------------------------------------- /cmake/common/ccache.cmake: -------------------------------------------------------------------------------- 1 | # OBS 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 "Trying to find ccache on build host - done") 13 | message(DEBUG "Ccache found as ${CCACHE_PROGRAM}") 14 | option(ENABLE_CCACHE "Enable compiler acceleration with ccache" OFF) 15 | 16 | if(ENABLE_CCACHE) 17 | set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") 18 | set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") 19 | set(CMAKE_OBJC_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") 20 | set(CMAKE_OBJCXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") 21 | set(CMAKE_CUDA_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") 22 | endif() 23 | else() 24 | message(DEBUG "Trying to find ccache on build host - skipped") 25 | endif() 26 | -------------------------------------------------------------------------------- /src/components/SliderIgnorewheel.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | class SliderIgnoreScroll : public QSlider { 9 | Q_OBJECT 10 | 11 | public: 12 | SliderIgnoreScroll(QWidget *parent = nullptr); 13 | SliderIgnoreScroll(Qt::Orientation orientation, QWidget *parent = nullptr); 14 | 15 | protected: 16 | virtual void wheelEvent(QWheelEvent *event) override; 17 | }; 18 | 19 | class SliderIgnoreClick : public SliderIgnoreScroll { 20 | public: 21 | inline SliderIgnoreClick(Qt::Orientation orientation, QWidget *parent = nullptr) 22 | : SliderIgnoreScroll(orientation, parent) 23 | { 24 | } 25 | 26 | protected: 27 | virtual void mousePressEvent(QMouseEvent *event) override; 28 | virtual void mouseReleaseEvent(QMouseEvent *event) override; 29 | virtual void mouseMoveEvent(QMouseEvent *event) override; 30 | 31 | private: 32 | bool dragging = false; 33 | }; 34 | -------------------------------------------------------------------------------- /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) 15 | if(EXISTS "${_BUILD_NUMBER_CACHE}") 16 | file(READ "${_BUILD_NUMBER_CACHE}" PLUGIN_BUILD_NUMBER) 17 | math(EXPR PLUGIN_BUILD_NUMBER "${PLUGIN_BUILD_NUMBER}+1") 18 | else() 19 | if("$ENV{CI}") 20 | if("$ENV{GITHUB_RUN_ID}") 21 | set(PLUGIN_BUILD_NUMBER "$ENV{GITHUB_RUN_ID}") 22 | elseif("$ENV{GITLAB_RUN_ID}") 23 | set(PLUGIN_BUILD_NUMBER "$ENV{GITLAB_RUN_ID}") 24 | else() 25 | set(PLUGIN_BUILD_NUMBER "1") 26 | endif() 27 | endif() 28 | endif() 29 | file(WRITE "${_BUILD_NUMBER_CACHE}" "${PLUGIN_BUILD_NUMBER}") 30 | endif() 31 | -------------------------------------------------------------------------------- /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/components/AbsoluteSlider.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "SliderIgnorewheel.hpp" 4 | 5 | class AbsoluteSlider : public SliderIgnoreScroll { 6 | Q_OBJECT 7 | Q_PROPERTY(QColor tickColor READ getTickColor WRITE setTickColor DESIGNABLE true) 8 | 9 | public: 10 | AbsoluteSlider(QWidget *parent = nullptr); 11 | AbsoluteSlider(Qt::Orientation orientation, QWidget *parent = nullptr); 12 | 13 | bool getDisplayTicks() const; 14 | void setDisplayTicks(bool display); 15 | 16 | QColor getTickColor() const; 17 | void setTickColor(QColor c); 18 | 19 | signals: 20 | void absoluteSliderHovered(int value); 21 | 22 | protected: 23 | virtual void mouseMoveEvent(QMouseEvent *event) override; 24 | virtual void mousePressEvent(QMouseEvent *event) override; 25 | virtual void mouseReleaseEvent(QMouseEvent *event) override; 26 | virtual bool eventFilter(QObject *obj, QEvent *event) override; 27 | 28 | int posToRangeValue(QMouseEvent *event); 29 | 30 | virtual void paintEvent(QPaintEvent *event) override; 31 | 32 | private: 33 | bool dragging = false; 34 | bool displayTicks = false; 35 | 36 | QColor tickColor; 37 | }; 38 | -------------------------------------------------------------------------------- /src/models/MediaData.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | class MediaObj : public QObject { 9 | Q_OBJECT 10 | 11 | private: 12 | static std::vector mediaItems; 13 | 14 | QString uuid; 15 | 16 | QString name = ""; 17 | QString path = ""; 18 | bool loop = false; 19 | float volume = 1.0f; 20 | 21 | obs_hotkey_id hotkey = OBS_INVALID_HOTKEY_ID; 22 | 23 | private slots: 24 | void pressed(); 25 | void released(); 26 | 27 | public: 28 | MediaObj(const QString &name, const QString &path); 29 | ~MediaObj(); 30 | 31 | static MediaObj *findByUUID(const QString &uuid); 32 | static MediaObj *findByName(const QString &name); 33 | 34 | QString getUUID(); 35 | 36 | void setName(const QString &newName); 37 | QString getName(); 38 | 39 | void setPath(const QString &newPath); 40 | QString getPath(); 41 | 42 | obs_hotkey_id getHotkey(); 43 | 44 | void setLoopEnabled(bool enable); 45 | bool loopEnabled(); 46 | 47 | void setVolume(float volume); 48 | float getVolume(); 49 | 50 | signals: 51 | void hotkeyPressed(MediaObj *obj); 52 | void hotkeyReleased(MediaObj *obj); 53 | 54 | void renamed(MediaObj *obj); 55 | }; 56 | -------------------------------------------------------------------------------- /.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-support.h: -------------------------------------------------------------------------------- 1 | /* 2 | Plugin Name 3 | Copyright (C) 2025 cg2121 claytong1214@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 | extern void blogva(int log_level, const char *format, va_list args); 35 | 36 | #ifdef __cplusplus 37 | } 38 | #endif 39 | -------------------------------------------------------------------------------- /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/plugin-support.c.in: -------------------------------------------------------------------------------- 1 | /* 2 | Plugin Name 3 | Copyright (C) 2025 cg2121 claytong1214@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 | const char *PLUGIN_NAME = "@CMAKE_PROJECT_NAME@"; 22 | const char *PLUGIN_VERSION = "@CMAKE_PROJECT_VERSION@"; 23 | 24 | void obs_log(int log_level, const char *format, ...) 25 | { 26 | size_t length = 4 + strlen(PLUGIN_NAME) + strlen(format); 27 | 28 | char *template = malloc(length + 1); 29 | 30 | snprintf(template, length, "[%s] %s", PLUGIN_NAME, format); 31 | 32 | va_list(args); 33 | 34 | va_start(args, format); 35 | blogva(log_level, template, args); 36 | va_end(args); 37 | 38 | free(template); 39 | } 40 | -------------------------------------------------------------------------------- /src/components/SliderIgnorewheel.cpp: -------------------------------------------------------------------------------- 1 | #include "moc_SliderIgnorewheel.cpp" 2 | 3 | SliderIgnoreScroll::SliderIgnoreScroll(QWidget *parent) : QSlider(parent) 4 | { 5 | setFocusPolicy(Qt::StrongFocus); 6 | } 7 | 8 | SliderIgnoreScroll::SliderIgnoreScroll(Qt::Orientation orientation, QWidget *parent) : QSlider(parent) 9 | { 10 | setFocusPolicy(Qt::StrongFocus); 11 | setOrientation(orientation); 12 | } 13 | 14 | void SliderIgnoreScroll::wheelEvent(QWheelEvent *event) 15 | { 16 | if (!hasFocus()) 17 | event->ignore(); 18 | else 19 | QSlider::wheelEvent(event); 20 | } 21 | 22 | void SliderIgnoreClick::mousePressEvent(QMouseEvent *event) 23 | { 24 | QStyleOptionSlider styleOption; 25 | initStyleOption(&styleOption); 26 | QRect handle = style()->subControlRect(QStyle::CC_Slider, &styleOption, QStyle::SC_SliderHandle, this); 27 | if (handle.contains(event->position().toPoint())) { 28 | SliderIgnoreScroll::mousePressEvent(event); 29 | dragging = true; 30 | } else { 31 | event->accept(); 32 | } 33 | } 34 | 35 | void SliderIgnoreClick::mouseReleaseEvent(QMouseEvent *event) 36 | { 37 | dragging = false; 38 | SliderIgnoreScroll::mouseReleaseEvent(event); 39 | } 40 | 41 | void SliderIgnoreClick::mouseMoveEvent(QMouseEvent *event) 42 | { 43 | if (dragging) { 44 | SliderIgnoreScroll::mouseMoveEvent(event); 45 | } else { 46 | event->accept(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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 | include(xcode) 16 | 17 | include(buildspec) 18 | 19 | # Use Applications directory as default install destination 20 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 21 | set( 22 | CMAKE_INSTALL_PREFIX 23 | "$ENV{HOME}/Library/Application Support/obs-studio/plugins" 24 | CACHE STRING 25 | "Default plugin installation directory" 26 | FORCE 27 | ) 28 | endif() 29 | 30 | # Enable find_package targets to become globally available targets 31 | set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL TRUE) 32 | # Enable RPATH support for generated binaries 33 | set(CMAKE_MACOSX_RPATH TRUE) 34 | # Use RPATHs from build tree _in_ the build tree 35 | set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) 36 | # Do not add default linker search paths to RPATH 37 | set(CMAKE_INSTALL_RPATH_USE_LINK_PATH FALSE) 38 | # Use common bundle-relative RPATH for installed targets 39 | set(CMAKE_INSTALL_RPATH "@executable_path/../Frameworks") 40 | -------------------------------------------------------------------------------- /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/components/SceneTree.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | class SceneTree : public QListWidget { 9 | Q_OBJECT 10 | Q_PROPERTY(int gridItemWidth READ GetGridItemWidth WRITE SetGridItemWidth DESIGNABLE true) 11 | Q_PROPERTY(int gridItemHeight READ GetGridItemHeight WRITE SetGridItemHeight DESIGNABLE true) 12 | 13 | bool gridMode = false; 14 | int maxWidth = 150; 15 | int itemHeight = 24; 16 | 17 | public: 18 | void SetGridMode(bool grid); 19 | bool GetGridMode(); 20 | 21 | void SetGridItemWidth(int width); 22 | void SetGridItemHeight(int height); 23 | int GetGridItemWidth(); 24 | int GetGridItemHeight(); 25 | 26 | explicit SceneTree(QWidget *parent = nullptr); 27 | 28 | private: 29 | void RepositionGrid(QDragMoveEvent *event = nullptr); 30 | 31 | protected: 32 | virtual bool eventFilter(QObject *obj, QEvent *event) override; 33 | virtual void resizeEvent(QResizeEvent *event) override; 34 | virtual void startDrag(Qt::DropActions supportedActions) override; 35 | virtual void dropEvent(QDropEvent *event) override; 36 | virtual void dragMoveEvent(QDragMoveEvent *event) override; 37 | virtual void dragLeaveEvent(QDragLeaveEvent *event) override; 38 | virtual void rowsInserted(const QModelIndex &parent, int start, int end) override; 39 | #if QT_VERSION < QT_VERSION_CHECK(6, 4, 3) 40 | virtual void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override; 41 | #endif 42 | 43 | signals: 44 | void scenesReordered(); 45 | }; 46 | -------------------------------------------------------------------------------- /.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 | obs-studio 45 | 46 | local -a _qt_packages=() 47 | 48 | if (( QT_VERSION == 5 )) { 49 | _qt_packages+=( 50 | qtbase5-dev${suffix} 51 | libqt5svg5-dev${suffix} 52 | qtbase5-private-dev${suffix} 53 | libqt5x11extras5-dev${suffix} 54 | ) 55 | } else { 56 | _qt_packages+=( 57 | qt6-base-dev${suffix} 58 | libqt6svg6-dev${suffix} 59 | qt6-base-private-dev${suffix} 60 | ) 61 | } 62 | 63 | sudo apt-get install ${apt_args} ${_qt_packages} 64 | log_group 65 | } 66 | -------------------------------------------------------------------------------- /.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/common/helpers_common.cmake: -------------------------------------------------------------------------------- 1 | # CMake common helper functions module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # check_uuid: Helper function to check for valid UUID 6 | function(check_uuid uuid_string return_value) 7 | set(valid_uuid TRUE) 8 | # gersemi: off 9 | set(uuid_token_lengths 8 4 4 4 12) 10 | # gersemi: on 11 | set(token_num 0) 12 | 13 | string(REPLACE "-" ";" uuid_tokens ${uuid_string}) 14 | list(LENGTH uuid_tokens uuid_num_tokens) 15 | 16 | if(uuid_num_tokens EQUAL 5) 17 | message(DEBUG "UUID ${uuid_string} is valid with 5 tokens.") 18 | foreach(uuid_token IN LISTS uuid_tokens) 19 | list(GET uuid_token_lengths ${token_num} uuid_target_length) 20 | string(LENGTH "${uuid_token}" uuid_actual_length) 21 | if(uuid_actual_length EQUAL uuid_target_length) 22 | string(REGEX MATCH "[0-9a-fA-F]+" uuid_hex_match ${uuid_token}) 23 | if(NOT uuid_hex_match STREQUAL uuid_token) 24 | set(valid_uuid FALSE) 25 | break() 26 | endif() 27 | else() 28 | set(valid_uuid FALSE) 29 | break() 30 | endif() 31 | math(EXPR token_num "${token_num}+1") 32 | endforeach() 33 | else() 34 | set(valid_uuid FALSE) 35 | endif() 36 | message(DEBUG "UUID ${uuid_string} valid: ${valid_uuid}") 37 | set(${return_value} ${valid_uuid} PARENT_SCOPE) 38 | endfunction() 39 | 40 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/plugin-support.c.in") 41 | configure_file(src/plugin-support.c.in plugin-support.c @ONLY) 42 | add_library(plugin-support STATIC) 43 | target_sources(plugin-support PRIVATE plugin-support.c PUBLIC src/plugin-support.h) 44 | target_include_directories(plugin-support PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") 45 | if(OS_LINUX OR OS_FREEBSD OR OS_OPENBSD) 46 | # add fPIC on Linux to prevent shared object errors 47 | set_property(TARGET plugin-support PROPERTY POSITION_INDEPENDENT_CODE ON) 48 | endif() 49 | endif() 50 | -------------------------------------------------------------------------------- /cmake/windows/compilerconfig.cmake: -------------------------------------------------------------------------------- 1 | # CMake Windows compiler configuration module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(compiler_common) 6 | 7 | set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT ProgramDatabase) 8 | 9 | message(DEBUG "Current Windows API version: ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}") 10 | if(CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION_MAXIMUM) 11 | message(DEBUG "Maximum Windows API version: ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION_MAXIMUM}") 12 | endif() 13 | 14 | if(CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION VERSION_LESS 10.0.20348) 15 | message( 16 | FATAL_ERROR 17 | "OBS requires Windows 10 SDK version 10.0.20348.0 or more recent.\n" 18 | "Please download and install the most recent Windows platform SDK." 19 | ) 20 | endif() 21 | 22 | set(_obs_msvc_c_options /MP /Zc:__cplusplus /Zc:preprocessor) 23 | set(_obs_msvc_cpp_options /MP /Zc:__cplusplus /Zc:preprocessor) 24 | 25 | if(CMAKE_CXX_STANDARD GREATER_EQUAL 20) 26 | list(APPEND _obs_msvc_cpp_options /Zc:char8_t-) 27 | endif() 28 | 29 | add_compile_options( 30 | /W3 31 | /utf-8 32 | /Brepro 33 | /permissive- 34 | "$<$:${_obs_msvc_c_options}>" 35 | "$<$:${_obs_msvc_cpp_options}>" 36 | "$<$:${_obs_clang_c_options}>" 37 | "$<$:${_obs_clang_cxx_options}>" 38 | $<$>:/Gy> 39 | $<$>:/GL> 40 | $<$>:/Oi> 41 | ) 42 | 43 | add_compile_definitions( 44 | UNICODE 45 | _UNICODE 46 | _CRT_SECURE_NO_WARNINGS 47 | _CRT_NONSTDC_NO_WARNINGS 48 | $<$:DEBUG> 49 | $<$:_DEBUG> 50 | ) 51 | 52 | add_link_options( 53 | $<$>:/OPT:REF> 54 | $<$>:/OPT:ICF> 55 | $<$>:/LTCG> 56 | $<$>:/INCREMENTAL:NO> 57 | /DEBUG 58 | /Brepro 59 | ) 60 | 61 | if(CMAKE_COMPILE_WARNING_AS_ERROR) 62 | add_link_options(/WX) 63 | endif() 64 | -------------------------------------------------------------------------------- /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/Soundboard.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | class MediaControls; 13 | class MediaObj; 14 | class QListWidgetItem; 15 | class SceneTree; 16 | class Ui_Soundboard; 17 | 18 | class Soundboard : public QWidget { 19 | Q_OBJECT 20 | 21 | private: 22 | QString prevPath; 23 | std::unique_ptr ui; 24 | 25 | MediaObj *getCurrentMediaObj(); 26 | QListWidgetItem *findItem(MediaObj *obj); 27 | 28 | OBSSourceAutoRelease source; 29 | 30 | bool actionsEnabled = false; 31 | 32 | QAction *renameMedia = nullptr; 33 | 34 | private slots: 35 | void on_list_itemClicked(); 36 | void on_actionAdd_triggered(); 37 | void on_actionRemove_triggered(); 38 | void on_actionEdit_triggered(); 39 | void updateActions(); 40 | void on_list_customContextMenuRequested(const QPoint &pos); 41 | void on_actionDuplicate_triggered(); 42 | 43 | MediaObj *add(const QString &name, const QString &path); 44 | void play(MediaObj *obj); 45 | 46 | void editMediaName(); 47 | void mediaNameEdited(QWidget *editor); 48 | 49 | void itemRenamed(MediaObj *obj); 50 | 51 | public: 52 | Soundboard(QWidget *parent = nullptr); 53 | ~Soundboard(); 54 | 55 | OBSDataArray saveMedia(); 56 | void loadMedia(OBSDataArray array); 57 | void loadSource(OBSData saveData); 58 | void clear(); 59 | 60 | void save(OBSData saveData); 61 | void load(OBSData saveData); 62 | 63 | void createSource(); 64 | 65 | protected: 66 | virtual void dragEnterEvent(QDragEnterEvent *event) override; 67 | virtual void dragLeaveEvent(QDragLeaveEvent *event) override; 68 | virtual void dragMoveEvent(QDragMoveEvent *event) override; 69 | virtual void dropEvent(QDropEvent *event) override; 70 | }; 71 | 72 | class MediaRenameDelegate : public QStyledItemDelegate { 73 | Q_OBJECT 74 | 75 | public: 76 | MediaRenameDelegate(QObject *parent); 77 | virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; 78 | 79 | protected: 80 | virtual bool eventFilter(QObject *editor, QEvent *event) override; 81 | }; 82 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.28...3.30) 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" ON) 8 | option(ENABLE_QT "Use Qt functionality" ON) 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 | set_property(TARGET ${CMAKE_PROJECT_NAME} APPEND PROPERTY AUTOUIC_SEARCH_PATHS src/forms) 36 | endif() 37 | 38 | target_sources( 39 | ${CMAKE_PROJECT_NAME} 40 | PRIVATE 41 | src/components/AbsoluteSlider.cpp 42 | src/components/AbsoluteSlider.hpp 43 | src/components/ClickableLabel.hpp 44 | src/components/SceneTree.cpp 45 | src/components/SceneTree.hpp 46 | src/components/SliderIgnorewheel.hpp 47 | src/components/SliderIgnorewheel.cpp 48 | src/components/MediaControls.cpp 49 | src/components/MediaControls.hpp 50 | src/dialogs/MediaEdit.hpp 51 | src/dialogs/MediaEdit.cpp 52 | src/models/MediaData.hpp 53 | src/models/MediaData.cpp 54 | src/forms/MediaControls.ui 55 | src/forms/MediaEdit.ui 56 | src/forms/Soundboard.ui 57 | src/Soundboard.hpp 58 | src/Soundboard.cpp 59 | src/plugin-support.c.in 60 | src/plugin-support.h 61 | ) 62 | 63 | set_target_properties_plugin(${CMAKE_PROJECT_NAME} PROPERTIES OUTPUT_NAME ${_name}) 64 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/components/MediaControls.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | class Ui_MediaControls; 9 | 10 | class MediaControls : public QWidget { 11 | Q_OBJECT 12 | 13 | friend class Soundboard; 14 | 15 | private: 16 | std::vector sigs; 17 | OBSWeakSource weakSource = nullptr; 18 | QTimer mediaTimer; 19 | QTimer seekTimer; 20 | int seek; 21 | int lastSeek; 22 | bool prevPaused = false; 23 | bool countDownTimer = false; 24 | bool isSlideshow = false; 25 | 26 | QString FormatSeconds(int totalSeconds); 27 | void StartMediaTimer(); 28 | void StopMediaTimer(); 29 | void RefreshControls(); 30 | void SetScene(OBSScene scene); 31 | int64_t GetSliderTime(int val); 32 | 33 | static void OBSMediaStopped(void *data, calldata_t *calldata); 34 | static void OBSMediaPlay(void *data, calldata_t *calldata); 35 | static void OBSMediaPause(void *data, calldata_t *calldata); 36 | static void OBSMediaStarted(void *data, calldata_t *calldata); 37 | static void OBSMediaNext(void *data, calldata_t *calldata); 38 | static void OBSMediaPrevious(void *data, calldata_t *calldata); 39 | 40 | std::unique_ptr ui; 41 | 42 | private slots: 43 | void on_playPauseButton_clicked(); 44 | void on_stopButton_clicked(); 45 | void on_nextButton_clicked(); 46 | void on_previousButton_clicked(); 47 | void on_durationLabel_clicked(); 48 | 49 | void AbsoluteSliderClicked(); 50 | void AbsoluteSliderReleased(); 51 | void AbsoluteSliderHovered(int val); 52 | void AbsoluteSliderMoved(int val); 53 | void SetSliderPosition(); 54 | void SetPlayingState(); 55 | void SetPausedState(); 56 | void SetRestartState(); 57 | void RestartMedia(); 58 | void StopMedia(); 59 | void PlaylistNext(); 60 | void PlaylistPrevious(); 61 | 62 | void SeekTimerCallback(); 63 | 64 | void MoveSliderFoward(int seconds = 5); 65 | void MoveSliderBackwards(int seconds = 5); 66 | 67 | void UpdateSlideCounter(); 68 | void UpdateLabels(int val); 69 | 70 | public slots: 71 | void PlayMedia(); 72 | void PauseMedia(); 73 | 74 | public: 75 | MediaControls(QWidget *parent = nullptr); 76 | ~MediaControls(); 77 | 78 | OBSSource GetSource(); 79 | void SetSource(OBSSource newSource); 80 | bool MediaPaused(); 81 | }; 82 | -------------------------------------------------------------------------------- /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 | set(CMAKE_C_STANDARD 17) 7 | set(CMAKE_C_STANDARD_REQUIRED TRUE) 8 | set(CMAKE_CXX_STANDARD 17) 9 | set(CMAKE_CXX_STANDARD_REQUIRED TRUE) 10 | 11 | # Set symbols to be hidden by default for C and C++ 12 | set(CMAKE_C_VISIBILITY_PRESET hidden) 13 | set(CMAKE_CXX_VISIBILITY_PRESET hidden) 14 | set(CMAKE_VISIBILITY_INLINES_HIDDEN TRUE) 15 | 16 | # clang options for C, C++, ObjC, and ObjC++ 17 | set( 18 | _obs_clang_common_options 19 | -fno-strict-aliasing 20 | -Wno-trigraphs 21 | -Wno-missing-field-initializers 22 | -Wno-missing-prototypes 23 | -Werror=return-type 24 | -Wunreachable-code 25 | -Wquoted-include-in-framework-header 26 | -Wno-missing-braces 27 | -Wparentheses 28 | -Wswitch 29 | -Wno-unused-function 30 | -Wno-unused-label 31 | -Wunused-parameter 32 | -Wunused-variable 33 | -Wunused-value 34 | -Wempty-body 35 | -Wuninitialized 36 | -Wno-unknown-pragmas 37 | -Wfour-char-constants 38 | -Wconstant-conversion 39 | -Wno-conversion 40 | -Wint-conversion 41 | -Wbool-conversion 42 | -Wenum-conversion 43 | -Wnon-literal-null-conversion 44 | -Wsign-compare 45 | -Wshorten-64-to-32 46 | -Wpointer-sign 47 | -Wnewline-eof 48 | -Wno-implicit-fallthrough 49 | -Wdeprecated-declarations 50 | -Wno-sign-conversion 51 | -Winfinite-recursion 52 | -Wcomma 53 | -Wno-strict-prototypes 54 | -Wno-semicolon-before-method-body 55 | -Wformat-security 56 | -Wvla 57 | -Wno-error=shorten-64-to-32 58 | ) 59 | 60 | # clang options for C 61 | set(_obs_clang_c_options ${_obs_clang_common_options} -Wno-shadow -Wno-float-conversion) 62 | 63 | # clang options for C++ 64 | set( 65 | _obs_clang_cxx_options 66 | ${_obs_clang_common_options} 67 | -Wno-non-virtual-dtor 68 | -Wno-overloaded-virtual 69 | -Wno-exit-time-destructors 70 | -Wno-shadow 71 | -Winvalid-offsetof 72 | -Wmove 73 | -Werror=block-capture-autoreleasing 74 | -Wrange-loop-analysis 75 | ) 76 | 77 | if(CMAKE_CXX_STANDARD GREATER_EQUAL 20) 78 | list(APPEND _obs_clang_cxx_options -fno-char8_t) 79 | endif() 80 | 81 | if(NOT DEFINED CMAKE_COMPILE_WARNING_AS_ERROR) 82 | set(CMAKE_COMPILE_WARNING_AS_ERROR ON) 83 | endif() 84 | -------------------------------------------------------------------------------- /.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@17/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-17 54 | brew install --quiet obsproject/tools/clang-format@17 55 | print ::endgroup:: 56 | 57 | print ::group::Run clang-format-17 58 | local -a changes=(${(s:,:)CHANGED_FILES//[\[\]\'\"]/}) 59 | ./build-aux/run-clang-format --fail-${{ inputs.failCondition }} --check ${changes} 60 | print ::endgroup:: 61 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/dialogs/MediaEdit.cpp: -------------------------------------------------------------------------------- 1 | #include "MediaEdit.hpp" 2 | #include "ui_MediaEdit.h" 3 | 4 | #include 5 | #include 6 | 7 | #include "models/MediaData.hpp" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "moc_MediaEdit.cpp" 14 | 15 | #define QTStr(str) QString(obs_module_text(str)) 16 | #define MainStr(str) QString(obs_frontend_get_locale_string(str)) 17 | 18 | MediaEdit::MediaEdit(QWidget *parent) : QDialog(parent), ui(new Ui_MediaEdit) 19 | { 20 | obs_frontend_push_ui_translation(obs_module_get_string); 21 | ui->setupUi(this); 22 | obs_frontend_pop_ui_translation(); 23 | } 24 | 25 | MediaEdit::~MediaEdit() {} 26 | 27 | void MediaEdit::setName(const QString &name) 28 | { 29 | ui->name->setText(name); 30 | 31 | if (origText.isNull()) 32 | origText = name; 33 | } 34 | 35 | QString MediaEdit::getName() 36 | { 37 | return ui->name->text(); 38 | } 39 | 40 | void MediaEdit::setPath(const QString &path) 41 | { 42 | ui->path->setText(path); 43 | } 44 | 45 | QString MediaEdit::getPath() 46 | { 47 | return ui->path->text(); 48 | } 49 | 50 | void MediaEdit::setLoopChecked(bool checked) 51 | { 52 | ui->loop->setChecked(checked); 53 | } 54 | 55 | bool MediaEdit::loopChecked() 56 | { 57 | return ui->loop->isChecked(); 58 | } 59 | 60 | void MediaEdit::on_browseButton_clicked() 61 | { 62 | QString folder = ui->path->text(); 63 | 64 | if (!folder.isEmpty()) 65 | folder = QFileInfo(folder).absoluteDir().absolutePath(); 66 | else 67 | folder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); 68 | 69 | QString fileName = QFileDialog::getOpenFileName(this, QTStr("OpenAudioFile"), folder, 70 | ("Audio (*.mp3 *.aac *.ogg *.wav *.flac)")); 71 | 72 | if (!fileName.isEmpty()) 73 | ui->path->setText(fileName); 74 | } 75 | 76 | void MediaEdit::on_buttonBox_clicked(QAbstractButton *button) 77 | { 78 | QDialogButtonBox::ButtonRole val = ui->buttonBox->buttonRole(button); 79 | 80 | if (val == QDialogButtonBox::RejectRole) { 81 | close(); 82 | } else if (val == QDialogButtonBox::AcceptRole) { 83 | QString name = getName(); 84 | 85 | if (name.isEmpty()) { 86 | QMessageBox::warning(this, MainStr("EmptyName.Title"), MainStr("EmptyName.Text")); 87 | return; 88 | } 89 | 90 | if (origText != name && MediaObj::findByName(name)) { 91 | QMessageBox::warning(this, MainStr("NameExists.Title"), MainStr("NameExists.Text")); 92 | return; 93 | } 94 | 95 | if (ui->path->text().isEmpty()) { 96 | QMessageBox::warning(this, QTStr("NoFile.Title"), QTStr("NoFile.Text")); 97 | return; 98 | } 99 | 100 | accept(); 101 | close(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /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 | -fno-strict-aliasing 15 | -fopenmp-simd 16 | -Wdeprecated-declarations 17 | -Wempty-body 18 | -Wenum-conversion 19 | -Werror=return-type 20 | -Wextra 21 | -Wformat 22 | -Wformat-security 23 | -Wno-conversion 24 | -Wno-float-conversion 25 | -Wno-implicit-fallthrough 26 | -Wno-missing-braces 27 | -Wno-missing-field-initializers 28 | -Wno-shadow 29 | -Wno-sign-conversion 30 | -Wno-trigraphs 31 | -Wno-unknown-pragmas 32 | -Wno-unused-function 33 | -Wno-unused-label 34 | -Wparentheses 35 | -Wuninitialized 36 | -Wunreachable-code 37 | -Wunused-parameter 38 | -Wunused-value 39 | -Wunused-variable 40 | -Wvla 41 | ) 42 | 43 | add_compile_options( 44 | -fopenmp-simd 45 | "$<$:${_obs_gcc_c_options}>" 46 | "$<$:-Wint-conversion;-Wno-missing-prototypes;-Wno-strict-prototypes;-Wpointer-sign>" 47 | "$<$:${_obs_gcc_c_options}>" 48 | "$<$:-Winvalid-offsetof;-Wno-overloaded-virtual>" 49 | "$<$:${_obs_clang_c_options}>" 50 | "$<$:${_obs_clang_cxx_options}>" 51 | ) 52 | 53 | if(CMAKE_CXX_COMPILER_ID STREQUAL GNU) 54 | # * Disable false-positive warning in GCC 12.1.0 and later 55 | # * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105562 56 | if(CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 12.1.0) 57 | add_compile_options(-Wno-error=maybe-uninitialized) 58 | endif() 59 | 60 | # * Add warning for infinite recursion (added in GCC 12) 61 | # * Also disable warnings for stringop-overflow due to https://gcc.gnu.org/bugzilla/show_bug.cgi?id=106297 62 | if(CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 12.0.0) 63 | add_compile_options(-Winfinite-recursion -Wno-stringop-overflow) 64 | endif() 65 | 66 | if(CMAKE_SYSTEM_PROCESSOR STREQUAL aarch64) 67 | add_compile_options(-Wno-error=type-limits) 68 | endif() 69 | endif() 70 | 71 | # Enable compiler and build tracing (requires Ninja generator) 72 | if(ENABLE_COMPILER_TRACE AND CMAKE_GENERATOR STREQUAL "Ninja") 73 | add_compile_options($<$:-ftime-trace> $<$:-ftime-trace>) 74 | else() 75 | set(ENABLE_COMPILER_TRACE OFF CACHE STRING "Enable Clang time-trace (required Clang and Ninja)" FORCE) 76 | endif() 77 | 78 | add_compile_definitions($<$:DEBUG> $<$:_DEBUG> SIMDE_ENABLE_OPENMP) 79 | -------------------------------------------------------------------------------- /cmake/common/bootstrap.cmake: -------------------------------------------------------------------------------- 1 | # Plugin bootstrap module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Map fallback configurations for optimized build configurations 6 | # gersemi: off 7 | set( 8 | CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO 9 | RelWithDebInfo 10 | Release 11 | MinSizeRel 12 | None 13 | "" 14 | ) 15 | set( 16 | CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL 17 | MinSizeRel 18 | Release 19 | RelWithDebInfo 20 | None 21 | "" 22 | ) 23 | set( 24 | CMAKE_MAP_IMPORTED_CONFIG_RELEASE 25 | Release 26 | RelWithDebInfo 27 | MinSizeRel 28 | None 29 | "" 30 | ) 31 | # gersemi: on 32 | 33 | # Prohibit in-source builds 34 | if("${CMAKE_CURRENT_BINARY_DIR}" STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}") 35 | message( 36 | FATAL_ERROR 37 | "In-source builds are not supported. " 38 | "Specify a build directory via 'cmake -S -B ' instead." 39 | ) 40 | file(REMOVE_RECURSE "${CMAKE_CURRENT_SOURCE_DIR}/CMakeCache.txt" "${CMAKE_CURRENT_SOURCE_DIR}/CMakeFiles") 41 | endif() 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 | string(JSON _name GET ${buildspec} name) 49 | string(JSON _website GET ${buildspec} website) 50 | string(JSON _author GET ${buildspec} author) 51 | string(JSON _email GET ${buildspec} email) 52 | string(JSON _version GET ${buildspec} version) 53 | string(JSON _bundleId GET ${buildspec} platformConfig macos bundleId) 54 | string(JSON _windowsAppUUID GET ${buildspec} platformConfig windows appUUID) 55 | 56 | set(PLUGIN_AUTHOR ${_author}) 57 | set(PLUGIN_WEBSITE ${_website}) 58 | set(PLUGIN_EMAIL ${_email}) 59 | set(PLUGIN_VERSION ${_version}) 60 | set(MACOS_BUNDLEID ${_bundleId}) 61 | set(WINDOWS_APPUUID ${_windowsAppUUID}) 62 | 63 | string(REPLACE "." ";" _version_canonical "${_version}") 64 | list(GET _version_canonical 0 PLUGIN_VERSION_MAJOR) 65 | list(GET _version_canonical 1 PLUGIN_VERSION_MINOR) 66 | list(GET _version_canonical 2 PLUGIN_VERSION_PATCH) 67 | unset(_version_canonical) 68 | 69 | include(buildnumber) 70 | include(osconfig) 71 | 72 | # Allow selection of common build types via UI 73 | if(NOT CMAKE_GENERATOR MATCHES "(Xcode|Visual Studio .+)") 74 | if(NOT CMAKE_BUILD_TYPE) 75 | set( 76 | CMAKE_BUILD_TYPE 77 | "RelWithDebInfo" 78 | CACHE STRING 79 | "OBS build type [Release, RelWithDebInfo, Debug, MinSizeRel]" 80 | FORCE 81 | ) 82 | set_property( 83 | CACHE CMAKE_BUILD_TYPE 84 | PROPERTY STRINGS Release RelWithDebInfo Debug MinSizeRel 85 | ) 86 | endif() 87 | endif() 88 | 89 | # Disable exports automatically going into the CMake package registry 90 | set(CMAKE_EXPORT_PACKAGE_REGISTRY FALSE) 91 | # Enable default inclusion of targets' source and binary directory 92 | set(CMAKE_INCLUDE_CURRENT_DIR TRUE) 93 | -------------------------------------------------------------------------------- /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 | #define MyWindowsAppUUID "@WINDOWS_APPUUID@" 6 | 7 | [Setup] 8 | ; NOTE: The value of AppId uniquely identifies this application. 9 | ; Do not use the same AppId value in installers for other applications. 10 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 11 | AppId={#MyWindowsAppUUID} 12 | AppName={#MyAppName} 13 | AppVersion={#MyAppVersion} 14 | AppPublisher={#MyAppPublisher} 15 | AppPublisherURL={#MyAppURL} 16 | AppSupportURL={#MyAppURL} 17 | AppUpdatesURL={#MyAppURL} 18 | DefaultDirName={commonappdata}\obs-studio\plugins 19 | DefaultGroupName={#MyAppName} 20 | OutputBaseFilename={#MyAppName}-{#MyAppVersion}-Windows-Installer 21 | Compression=lzma 22 | SolidCompression=yes 23 | DirExistsWarning=no 24 | 25 | [Languages] 26 | Name: "english"; MessagesFile: "compiler:Default.isl" 27 | 28 | [Files] 29 | Source: "..\release\Package\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 30 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 31 | 32 | [Icons] 33 | Name: "{group}\{cm:ProgramOnTheWeb,{#MyAppName}}"; Filename: "{#MyAppURL}" 34 | 35 | [Code] 36 | // Helper script to find the OBS installation path 37 | // credit where it's due : 38 | // following function comes from https://github.com/Xaymar/obs-studio_amf-encoder-plugin/blob/master/%23Resources/Installer.in.iss#L45 39 | // This is deprecated as it is not super reliable if portable installs are used. Replace with a more reliable method in the future. 40 | function GetOBSDirName(Value: String): String; 41 | var 42 | InstallPath: String; 43 | begin 44 | // initialize default path, which will be returned when the following registry 45 | // key queries fail due to missing keys or for some different reason 46 | Result := '{autopf}\obs-studio'; 47 | // query the first registry value; if this succeeds, return the obtained value 48 | if RegQueryStringValue(HKLM32, 'SOFTWARE\OBS Studio', '', InstallPath) then 49 | Result := InstallPath; 50 | end; 51 | 52 | // Pre-Post-Install actions 53 | procedure CurStepChanged(CurStep: TSetupStep); 54 | var 55 | ResultCode: Integer; 56 | begin 57 | ResultCode := 0; 58 | 59 | // Post-install Steps 60 | if ( CurStep = ssPostInstall ) then 61 | begin 62 | if ( DelTree(ExpandConstant('{code:GetOBSDirName}\data\obs-plugins\{#MyAppName}'), True, True, True) ) then 63 | Log('Removed old {#MyAppName} plugin folder: ' + ExpandConstant('{code:GetOBSDirName}\data\obs-plugins\{#MyAppName}')); 64 | if ( DelTree(ExpandConstant('{code:GetOBSDirName}\obs-plugins\64bit\{#MyAppName}*'), False, True, True) ) then 65 | Log('Removed old {#MyAppName} plugin files: ' + ExpandConstant('{code:GetOBSDirName}\obs-plugins\64bit\{#MyAppName}*')); 66 | end; 67 | end; 68 | -------------------------------------------------------------------------------- /cmake/linux/defaults.cmake: -------------------------------------------------------------------------------- 1 | # CMake Linux defaults module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Set default installation directories 6 | include(GNUInstallDirs) 7 | 8 | if(CMAKE_INSTALL_LIBDIR MATCHES "(CMAKE_SYSTEM_PROCESSOR)") 9 | string(REPLACE "CMAKE_SYSTEM_PROCESSOR" "${CMAKE_SYSTEM_PROCESSOR}" CMAKE_INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}") 10 | endif() 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 | ".*~$" 34 | \\.git/ 35 | \\.github/ 36 | \\.gitignore 37 | \\.ccache/ 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 | -------------------------------------------------------------------------------- /src/models/MediaData.cpp: -------------------------------------------------------------------------------- 1 | #include "MediaData.hpp" 2 | #include 3 | #include 4 | #include 5 | 6 | #define QTStr(str) QString(obs_module_text(str)) 7 | #define QT_UTF8(str) QString::fromUtf8(str, -1) 8 | #define QT_TO_UTF8(str) str.toUtf8().constData() 9 | 10 | std::vector MediaObj::mediaItems; 11 | 12 | MediaObj::MediaObj(const QString &name_, const QString &path_) : name(name_), path(path_) 13 | { 14 | BPtr uuid_ = os_generate_uuid(); 15 | uuid = uuid_.Get(); 16 | 17 | QString hotkeyName = QTStr("SoundHotkey").arg(name); 18 | 19 | auto playSound = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { 20 | MediaObj *sound = static_cast(data); 21 | 22 | if (pressed) 23 | QMetaObject::invokeMethod(sound, &MediaObj::pressed); 24 | else 25 | QMetaObject::invokeMethod(sound, &MediaObj::released); 26 | }; 27 | 28 | hotkey = obs_hotkey_register_frontend(QT_TO_UTF8(hotkeyName), QT_TO_UTF8(hotkeyName), playSound, this); 29 | 30 | mediaItems.emplace_back(this); 31 | } 32 | 33 | MediaObj::~MediaObj() 34 | { 35 | obs_hotkey_unregister(hotkey); 36 | mediaItems.erase(std::remove(mediaItems.begin(), mediaItems.end(), this), mediaItems.end()); 37 | } 38 | 39 | MediaObj *MediaObj::findByName(const QString &name) 40 | { 41 | for (auto &media : mediaItems) { 42 | if (media->name == name) 43 | return media; 44 | } 45 | 46 | return nullptr; 47 | } 48 | 49 | MediaObj *MediaObj::findByUUID(const QString &uuid) 50 | { 51 | for (auto &media : mediaItems) { 52 | if (media->uuid == uuid) 53 | return media; 54 | } 55 | 56 | return nullptr; 57 | } 58 | 59 | QString MediaObj::getUUID() 60 | { 61 | return uuid; 62 | } 63 | 64 | void MediaObj::setName(const QString &newName) 65 | { 66 | if (newName.isEmpty() || name == newName) 67 | return; 68 | 69 | name = newName; 70 | 71 | QString hotkeyName = QTStr("SoundHotkey").arg(name); 72 | obs_hotkey_set_name(hotkey, QT_TO_UTF8(hotkeyName)); 73 | obs_hotkey_set_description(hotkey, QT_TO_UTF8(hotkeyName)); 74 | 75 | emit renamed(this); 76 | } 77 | 78 | QString MediaObj::getName() 79 | { 80 | return name; 81 | } 82 | 83 | void MediaObj::setPath(const QString &newPath) 84 | { 85 | path = newPath; 86 | } 87 | 88 | QString MediaObj::getPath() 89 | { 90 | return path; 91 | } 92 | 93 | obs_hotkey_id MediaObj::getHotkey() 94 | { 95 | return hotkey; 96 | } 97 | 98 | void MediaObj::setLoopEnabled(bool enable) 99 | { 100 | loop = enable; 101 | } 102 | 103 | bool MediaObj::loopEnabled() 104 | { 105 | return loop; 106 | } 107 | 108 | void MediaObj::setVolume(float newVolume) 109 | { 110 | volume = newVolume; 111 | } 112 | 113 | float MediaObj::getVolume() 114 | { 115 | return volume; 116 | } 117 | 118 | void MediaObj::pressed() 119 | { 120 | emit hotkeyPressed(this); 121 | } 122 | 123 | void MediaObj::released() 124 | { 125 | emit hotkeyReleased(this); 126 | } 127 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | workingDirectory: 20 | description: Working directory for packaging 21 | required: false 22 | default: ${{ github.workspace }} 23 | runs: 24 | using: composite 25 | steps: 26 | - name: Run macOS Build 27 | if: runner.os == 'macOS' 28 | shell: zsh --no-rcs --errexit --pipefail {0} 29 | working-directory: ${{ inputs.workingDirectory }} 30 | env: 31 | CCACHE_DIR: ${{ inputs.workingDirectory }}/.ccache 32 | CODESIGN_IDENT: ${{ inputs.codesignIdent }} 33 | CODESIGN_TEAM: ${{ inputs.codesignTeam }} 34 | run: | 35 | : Run macOS Build 36 | 37 | local -a build_args=(--config ${{ inputs.config }}) 38 | if (( ${+RUNNER_DEBUG} )) build_args+=(--debug) 39 | 40 | if [[ '${{ inputs.codesign }}' == 'true' ]] build_args+=(--codesign) 41 | 42 | .github/scripts/build-macos ${build_args} 43 | 44 | - name: Install Dependencies 🛍️ 45 | if: runner.os == 'Linux' 46 | shell: bash 47 | run: | 48 | : Install Dependencies 🛍️ 49 | echo ::group::Install Dependencies 50 | eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 51 | echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH 52 | brew install --quiet zsh 53 | echo ::endgroup:: 54 | 55 | - name: Run Ubuntu Build 56 | if: runner.os == 'Linux' 57 | shell: zsh --no-rcs --errexit --pipefail {0} 58 | working-directory: ${{ inputs.workingDirectory }} 59 | env: 60 | CCACHE_DIR: ${{ inputs.workingDirectory }}/.ccache 61 | run: | 62 | : Run Ubuntu Build 63 | 64 | local -a build_args=( 65 | --target ubuntu-${{ inputs.target }} 66 | --config ${{ inputs.config }} 67 | ) 68 | if (( ${+RUNNER_DEBUG} )) build_args+=(--debug) 69 | 70 | .github/scripts/build-ubuntu ${build_args} 71 | 72 | - name: Run Windows Build 73 | if: runner.os == 'Windows' 74 | shell: pwsh 75 | run: | 76 | # Run Windows Build 77 | if ( $Env:RUNNER_DEBUG -ne $null ) { 78 | Set-PSDebug -Trace 1 79 | } 80 | 81 | $BuildArgs = @{ 82 | Target = '${{ inputs.target }}' 83 | Configuration = '${{ inputs.config }}' 84 | } 85 | 86 | .github/scripts/Build-Windows.ps1 @BuildArgs 87 | 88 | - name: Create Summary 📊 89 | if: contains(fromJSON('["Linux", "macOS"]'),runner.os) 90 | shell: zsh --no-rcs --errexit --pipefail {0} 91 | env: 92 | CCACHE_DIR: ${{ inputs.workingDirectory }}/.ccache 93 | run: | 94 | : Create Summary 📊 95 | 96 | local -a ccache_data 97 | if (( ${+RUNNER_DEBUG} )) { 98 | setopt XTRACE 99 | ccache_data=("${(fA)$(ccache -s -vv)}") 100 | } else { 101 | ccache_data=("${(fA)$(ccache -s)}") 102 | } 103 | 104 | print '### ${{ runner.os }} Ccache Stats (${{ inputs.target }})' >> $GITHUB_STEP_SUMMARY 105 | print '```' >> $GITHUB_STEP_SUMMARY 106 | for line (${ccache_data}) { 107 | print ${line} >> $GITHUB_STEP_SUMMARY 108 | } 109 | print '```' >> $GITHUB_STEP_SUMMARY 110 | -------------------------------------------------------------------------------- /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 ${PLUGIN_VERSION} SOVERSION ${PLUGIN_VERSION_MAJOR} 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 | add_custom_command( 37 | TARGET ${target} 38 | POST_BUILD 39 | COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$" 40 | COMMAND 41 | "${CMAKE_COMMAND}" -E copy_if_different "$" "${CMAKE_CURRENT_BINARY_DIR}/rundir/$" 42 | COMMENT "Copy ${target} to rundir" 43 | VERBATIM 44 | ) 45 | 46 | target_install_resources(${target}) 47 | 48 | get_target_property(target_sources ${target} SOURCES) 49 | set(target_ui_files ${target_sources}) 50 | list(FILTER target_ui_files INCLUDE REGEX ".+\\.(ui|qrc)") 51 | source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "UI Files" FILES ${target_ui_files}) 52 | endfunction() 53 | 54 | # Helper function to add resources into bundle 55 | function(target_install_resources target) 56 | message(DEBUG "Installing resources for target ${target}...") 57 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/data") 58 | file(GLOB_RECURSE data_files "${CMAKE_CURRENT_SOURCE_DIR}/data/*") 59 | foreach(data_file IN LISTS data_files) 60 | cmake_path( 61 | RELATIVE_PATH 62 | data_file 63 | BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" 64 | OUTPUT_VARIABLE relative_path 65 | ) 66 | cmake_path(GET relative_path PARENT_PATH relative_path) 67 | target_sources(${target} PRIVATE "${data_file}") 68 | source_group("Resources/${relative_path}" FILES "${data_file}") 69 | endforeach() 70 | 71 | install( 72 | DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" 73 | DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/obs/obs-plugins/${target} 74 | USE_SOURCE_PERMISSIONS 75 | ) 76 | 77 | add_custom_command( 78 | TARGET ${target} 79 | POST_BUILD 80 | COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 81 | COMMAND 82 | "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/data" 83 | "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 84 | COMMENT "Copy ${target} resources to rundir" 85 | VERBATIM 86 | ) 87 | endif() 88 | endfunction() 89 | 90 | # Helper function to add a specific resource to a bundle 91 | function(target_add_resource target resource) 92 | message(DEBUG "Add resource '${resource}' to target ${target} at destination '${target_destination}'...") 93 | 94 | install(FILES "${resource}" DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/obs/obs-plugins/${target}) 95 | 96 | add_custom_command( 97 | TARGET ${target} 98 | POST_BUILD 99 | COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 100 | COMMAND "${CMAKE_COMMAND}" -E copy "${resource}" "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 101 | COMMENT "Copy ${target} resource ${resource} to rundir" 102 | VERBATIM 103 | ) 104 | 105 | source_group("Resources" FILES "${resource}") 106 | endfunction() 107 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | 'windows-x64-Portable;zip|exe' 77 | 'macos-universal;tar.xz|pkg' 78 | 'ubuntu-24.04-x86_64;tar.xz|deb|ddeb' 79 | 'sources;tar.xz' 80 | ) 81 | 82 | for variant_data in "${variants[@]}"; do 83 | IFS=';' read -r variant suffix <<< "${variant_data}" 84 | 85 | candidates=(*-${variant}-${commit_hash}/@(*|*-dbgsym).@(${suffix})) 86 | 87 | for candidate in "${candidates[@]}"; do 88 | mv "${candidate}" "${root_dir}" 89 | done 90 | done 91 | 92 | - name: Generate Checksums 🪪 93 | if: fromJSON(steps.check.outputs.validTag) 94 | run: | 95 | : Generate Checksums 🪪 96 | if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi 97 | shopt -s extglob 98 | 99 | echo "### Checksums" > ${{ github.workspace }}/CHECKSUMS.txt 100 | for file in ${{ github.workspace }}/@(*.exe|*.deb|*.ddeb|*.pkg|*.tar.xz|*.zip); do 101 | echo " ${file##*/}: $(sha256sum "${file}" | cut -d " " -f 1)" >> ${{ github.workspace }}/CHECKSUMS.txt 102 | done 103 | 104 | - name: Create Release 🛫 105 | if: fromJSON(steps.check.outputs.validTag) 106 | id: create_release 107 | uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 108 | with: 109 | draft: true 110 | prerelease: ${{ fromJSON(steps.check.outputs.prerelease) }} 111 | tag_name: ${{ steps.check.outputs.version }} 112 | name: ${{ needs.build-project.outputs.pluginName }} ${{ steps.check.outputs.version }} 113 | body_path: ${{ github.workspace }}/CHECKSUMS.txt 114 | files: | 115 | ${{ github.workspace }}/*.exe 116 | ${{ github.workspace }}/*.zip 117 | ${{ github.workspace }}/*.pkg 118 | ${{ github.workspace }}/*.deb 119 | ${{ github.workspace }}/*.ddeb 120 | ${{ github.workspace }}/*.tar.xz 121 | -------------------------------------------------------------------------------- /cmake/macos/helpers.cmake: -------------------------------------------------------------------------------- 1 | # CMake macOS helper functions module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(helpers_common) 6 | 7 | # set_target_properties_obs: 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 | string(TIMESTAMP CURRENT_YEAR "%Y") 22 | set_target_properties( 23 | ${target} 24 | PROPERTIES 25 | BUNDLE TRUE 26 | BUNDLE_EXTENSION plugin 27 | XCODE_ATTRIBUTE_PRODUCT_NAME ${target} 28 | XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER ${MACOS_BUNDLEID} 29 | XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION ${PLUGIN_BUILD_NUMBER} 30 | XCODE_ATTRIBUTE_MARKETING_VERSION ${PLUGIN_VERSION} 31 | XCODE_ATTRIBUTE_GENERATE_INFOPLIST_FILE YES 32 | XCODE_ATTRIBUTE_INFOPLIST_FILE "" 33 | XCODE_ATTRIBUTE_INFOPLIST_KEY_CFBundleDisplayName ${target} 34 | XCODE_ATTRIBUTE_INFOPLIST_KEY_NSHumanReadableCopyright "(c) ${CURRENT_YEAR} ${PLUGIN_AUTHOR}" 35 | XCODE_ATTRIBUTE_INSTALL_PATH "$(USER_LIBRARY_DIR)/Application Support/obs-studio/plugins" 36 | ) 37 | 38 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/entitlements.plist") 39 | set_target_properties( 40 | ${target} 41 | PROPERTIES XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/entitlements.plist" 42 | ) 43 | endif() 44 | 45 | if(TARGET plugin-support) 46 | target_link_libraries(${target} PRIVATE plugin-support) 47 | endif() 48 | 49 | target_install_resources(${target}) 50 | 51 | add_custom_command( 52 | TARGET ${target} 53 | POST_BUILD 54 | COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$" 55 | COMMAND 56 | "${CMAKE_COMMAND}" -E copy_directory "$" 57 | "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/$" 58 | COMMENT "Copy ${target} to rundir" 59 | VERBATIM 60 | ) 61 | 62 | get_target_property(target_sources ${target} SOURCES) 63 | set(target_ui_files ${target_sources}) 64 | list(FILTER target_ui_files INCLUDE REGEX ".+\\.(ui|qrc)") 65 | source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "UI Files" FILES ${target_ui_files}) 66 | 67 | install(TARGETS ${target} LIBRARY DESTINATION .) 68 | install(FILES "$.dsym" CONFIGURATIONS Release DESTINATION . OPTIONAL) 69 | 70 | configure_file(cmake/macos/resources/distribution.in "${CMAKE_CURRENT_BINARY_DIR}/distribution" @ONLY) 71 | configure_file(cmake/macos/resources/create-package.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/create-package.cmake" @ONLY) 72 | install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/create-package.cmake") 73 | endfunction() 74 | 75 | # target_install_resources: Helper function to add resources into bundle 76 | function(target_install_resources target) 77 | message(DEBUG "Installing resources for target ${target}...") 78 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/data") 79 | file(GLOB_RECURSE data_files "${CMAKE_CURRENT_SOURCE_DIR}/data/*") 80 | foreach(data_file IN LISTS data_files) 81 | cmake_path( 82 | RELATIVE_PATH 83 | data_file 84 | BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" 85 | OUTPUT_VARIABLE relative_path 86 | ) 87 | cmake_path(GET relative_path PARENT_PATH relative_path) 88 | target_sources(${target} PRIVATE "${data_file}") 89 | set_property(SOURCE "${data_file}" PROPERTY MACOSX_PACKAGE_LOCATION "Resources/${relative_path}") 90 | source_group("Resources/${relative_path}" FILES "${data_file}") 91 | endforeach() 92 | endif() 93 | endfunction() 94 | 95 | # target_add_resource: Helper function to add a specific resource to a bundle 96 | function(target_add_resource target resource) 97 | message(DEBUG "Add resource ${resource} to target ${target} at destination ${destination}...") 98 | target_sources(${target} PRIVATE "${resource}") 99 | set_property(SOURCE "${resource}" PROPERTY MACOSX_PACKAGE_LOCATION Resources) 100 | source_group("Resources" FILES "${resource}") 101 | endfunction() 102 | -------------------------------------------------------------------------------- /.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 "Xcode and SDK Info" 114 | export SDKROOT="$(xcrun --sdk macosx --show-sdk-path)" 115 | export DEVELOPER_DIR="/Applications/Xcode_16.1.0.app/Contents/Developer" 116 | xcodebuild -version 117 | xcodebuild -showsdks 118 | xcode-select -p 119 | xcrun --show-sdk-version 120 | 121 | log_group "Configuring ${product_name}..." 122 | cmake -S ${project_root} ${cmake_args} 123 | 124 | log_group "Building ${product_name}..." 125 | run_xcodebuild() { 126 | if (( debug )) { 127 | xcodebuild ${@} 128 | } else { 129 | if [[ ${GITHUB_EVENT_NAME} == push ]] { 130 | xcodebuild ${@} 2>&1 | xcbeautify --renderer terminal 131 | } else { 132 | xcodebuild ${@} 2>&1 | xcbeautify --renderer github-actions 133 | } 134 | } 135 | } 136 | 137 | local -a build_args=( 138 | ONLY_ACTIVE_ARCH=NO 139 | -arch arm64 140 | -arch x86_64 141 | -project ${product_name}.xcodeproj 142 | -target ${product_name} 143 | -destination "generic/platform=macOS,name=Any Mac" 144 | -configuration ${config} 145 | -parallelizeTargets 146 | -hideShellScriptEnvironment 147 | build 148 | ) 149 | 150 | pushd build_macos 151 | run_xcodebuild ${build_args} 152 | popd 153 | 154 | log_group "Installing ${product_name}..." 155 | cmake --install build_macos --config ${config} --prefix "${project_root}/release/${config}" 156 | 157 | popd 158 | log_group 159 | } 160 | 161 | build ${@} 162 | -------------------------------------------------------------------------------- /.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 | "${ProjectRoot}/release/${ProductName}-*-windows-*.exe" 57 | ) 58 | } 59 | 60 | Remove-Item @RemoveArgs 61 | 62 | Log-Group "Archiving ${ProductName}..." 63 | $CompressArgs = @{ 64 | Path = (Get-ChildItem -Path "${ProjectRoot}/release/${Configuration}" -Exclude "${OutputName}*.*") 65 | CompressionLevel = 'Optimal' 66 | DestinationPath = "${ProjectRoot}/release/${OutputName}.zip" 67 | Verbose = ($Env:CI -ne $null) 68 | } 69 | Compress-Archive -Force @CompressArgs 70 | Log-Group 71 | 72 | Log-Group "Archiving Portable ${ProductName}..." 73 | tree /F "${ProjectRoot}/release/${Configuration}/${ProductName}" 74 | Copy-Item -Path "${ProjectRoot}/release/${Configuration}/${ProductName}/data/locale" -Destination "${ProjectRoot}/release-portable/${Configuration}/data/obs-plugins/${ProductName}/locale" -Recurse 75 | Copy-Item -Path "${ProjectRoot}/release/${Configuration}/${ProductName}/bin" -Destination "${ProjectRoot}/release-portable/${Configuration}/obs-plugins" -Recurse 76 | tree /F "${ProjectRoot}/release-portable/${Configuration}" 77 | 78 | $CompressArgs = @{ 79 | Path = (Get-ChildItem -Path "${ProjectRoot}/release-portable/${Configuration}" -Exclude "${OutputName}*.*") 80 | CompressionLevel = 'Optimal' 81 | DestinationPath = "${ProjectRoot}/release-portable/${OutputName}-Portable.zip" 82 | Verbose = ($Env:CI -ne $null) 83 | } 84 | Compress-Archive -Force @CompressArgs 85 | Log-Group 86 | 87 | # Declare the location of the InnoSetup setup file 88 | $IsccFile = "${ProjectRoot}/build_${Target}/installer-Windows.generated.iss" 89 | 90 | # Throw an error if the provided path is invalid 91 | if ( ! ( Test-Path -Path $IsccFile ) ) { 92 | throw 'InnoSetup install script not found. Run the build script or the CMake build and install procedures first.' 93 | } 94 | 95 | Log-Information 'Creating InnoSetup installer...' 96 | 97 | # Push the current location on the "BuildTemp" directory stack for easier return later 98 | Push-Location -Stack BuildTemp 99 | 100 | # Change to "release" sub-directory of the project root directory 101 | Ensure-Location -Path "${ProjectRoot}/release" 102 | 103 | # Copy the directory for the specified configuration (e.g. "Release") to a new directory named "Package" 104 | Copy-Item -Path ${Configuration} -Destination Package -Recurse 105 | 106 | # Invoke the InnoSetup iscc compiler with the specified setup file and the sub-directory "release" in 107 | # the project root directory as the output directory 108 | Invoke-External iscc ${IsccFile} /O"${ProjectRoot}/release" /F"${OutputName}-Installer" 109 | 110 | # Remove the copied "Package" directory and its contents 111 | Remove-Item -Path Package -Recurse 112 | 113 | # Pop the location stored in the "BuildTemp" directory stack earlier 114 | Pop-Location -Stack BuildTemp 115 | 116 | Log-Group 117 | } 118 | 119 | Package 120 | -------------------------------------------------------------------------------- /cmake/windows/helpers.cmake: -------------------------------------------------------------------------------- 1 | # CMake Windows 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 | string(TIMESTAMP CURRENT_YEAR "%Y") 22 | 23 | set_target_properties(${target} PROPERTIES VERSION 0 SOVERSION ${PLUGIN_VERSION}) 24 | 25 | install(TARGETS ${target} RUNTIME DESTINATION "${target}/bin/64bit" LIBRARY DESTINATION "${target}/bin/64bit") 26 | 27 | install( 28 | FILES "$" 29 | CONFIGURATIONS RelWithDebInfo Debug Release 30 | DESTINATION "${target}/bin/64bit" 31 | OPTIONAL 32 | ) 33 | 34 | if(TARGET plugin-support) 35 | target_link_libraries(${target} PRIVATE plugin-support) 36 | endif() 37 | 38 | add_custom_command( 39 | TARGET ${target} 40 | POST_BUILD 41 | COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$" 42 | COMMAND 43 | "${CMAKE_COMMAND}" -E copy_if_different "$" 44 | "$<$:$>" 45 | "${CMAKE_CURRENT_BINARY_DIR}/rundir/$" 46 | COMMENT "Copy ${target} to rundir" 47 | VERBATIM 48 | ) 49 | 50 | target_install_resources(${target}) 51 | 52 | get_target_property(target_sources ${target} SOURCES) 53 | set(target_ui_files ${target_sources}) 54 | list(FILTER target_ui_files INCLUDE REGEX ".+\\.(ui|qrc)") 55 | source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "UI Files" FILES ${target_ui_files}) 56 | 57 | set(valid_uuid FALSE) 58 | check_uuid(${_windowsAppUUID} valid_uuid) 59 | if(NOT valid_uuid) 60 | message(FATAL_ERROR "Specified Windows package UUID is not a valid UUID value: ${_windowsAppUUID}") 61 | else() 62 | set(UUID_APP ${_windowsAppUUID}) 63 | endif() 64 | 65 | configure_file( 66 | cmake/windows/resources/installer-Windows.iss.in 67 | "${CMAKE_CURRENT_BINARY_DIR}/installer-Windows.generated.iss" 68 | ) 69 | 70 | configure_file(cmake/windows/resources/resource.rc.in "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.rc") 71 | target_sources(${CMAKE_PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.rc") 72 | endfunction() 73 | 74 | # Helper function to add resources into bundle 75 | function(target_install_resources target) 76 | message(DEBUG "Installing resources for target ${target}...") 77 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/data") 78 | file(GLOB_RECURSE data_files "${CMAKE_CURRENT_SOURCE_DIR}/data/*") 79 | foreach(data_file IN LISTS data_files) 80 | cmake_path( 81 | RELATIVE_PATH 82 | data_file 83 | BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" 84 | OUTPUT_VARIABLE relative_path 85 | ) 86 | cmake_path(GET relative_path PARENT_PATH relative_path) 87 | target_sources(${target} PRIVATE "${data_file}") 88 | source_group("Resources/${relative_path}" FILES "${data_file}") 89 | endforeach() 90 | 91 | install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" DESTINATION "${target}/data" USE_SOURCE_PERMISSIONS) 92 | 93 | add_custom_command( 94 | TARGET ${target} 95 | POST_BUILD 96 | COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 97 | COMMAND 98 | "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/data" 99 | "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 100 | COMMENT "Copy ${target} resources to rundir" 101 | VERBATIM 102 | ) 103 | endif() 104 | endfunction() 105 | 106 | # Helper function to add a specific resource to a bundle 107 | function(target_add_resource target resource) 108 | message(DEBUG "Add resource '${resource}' to target ${target} at destination '${target_destination}'...") 109 | 110 | install(FILES "${resource}" DESTINATION "${target}/data" COMPONENT Runtime) 111 | 112 | add_custom_command( 113 | TARGET ${target} 114 | POST_BUILD 115 | COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 116 | COMMAND "${CMAKE_COMMAND}" -E copy "${resource}" "${CMAKE_CURRENT_BINARY_DIR}/rundir/$/${target}" 117 | COMMENT "Copy ${target} resource ${resource} to rundir" 118 | VERBATIM 119 | ) 120 | source_group("Resources" FILES "${resource}") 121 | endfunction() 122 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 28, 6 | "patch": 0 7 | }, 8 | "configurePresets": [ 9 | { 10 | "name": "template", 11 | "hidden": true, 12 | "cacheVariables": { 13 | "ENABLE_FRONTEND_API": true, 14 | "ENABLE_QT": true 15 | } 16 | }, 17 | { 18 | "name": "macos", 19 | "displayName": "macOS Universal", 20 | "description": "Build for macOS 11.0+ (Universal binary)", 21 | "inherits": ["template"], 22 | "binaryDir": "${sourceDir}/build_macos", 23 | "condition": { 24 | "type": "equals", 25 | "lhs": "${hostSystemName}", 26 | "rhs": "Darwin" 27 | }, 28 | "generator": "Xcode", 29 | "warnings": {"dev": true, "deprecated": true}, 30 | "cacheVariables": { 31 | "CMAKE_OSX_DEPLOYMENT_TARGET": "11.0", 32 | "CMAKE_OSX_ARCHITECTURES": "arm64;x86_64", 33 | "CODESIGN_IDENTITY": "$penv{CODESIGN_IDENT}", 34 | "CODESIGN_TEAM": "$penv{CODESIGN_TEAM}" 35 | } 36 | }, 37 | { 38 | "name": "macos-ci", 39 | "inherits": ["macos"], 40 | "displayName": "macOS Universal CI build", 41 | "description": "Build for macOS 11.0+ (Universal binary) for CI", 42 | "generator": "Xcode", 43 | "cacheVariables": { 44 | "CMAKE_COMPILE_WARNING_AS_ERROR": true, 45 | "ENABLE_CCACHE": true 46 | } 47 | }, 48 | { 49 | "name": "windows-x64", 50 | "displayName": "Windows x64", 51 | "description": "Build for Windows x64", 52 | "inherits": ["template"], 53 | "binaryDir": "${sourceDir}/build_x64", 54 | "condition": { 55 | "type": "equals", 56 | "lhs": "${hostSystemName}", 57 | "rhs": "Windows" 58 | }, 59 | "generator": "Visual Studio 17 2022", 60 | "architecture": "x64,version=10.0.22621", 61 | "warnings": {"dev": true, "deprecated": true} 62 | }, 63 | { 64 | "name": "windows-ci-x64", 65 | "inherits": ["windows-x64"], 66 | "displayName": "Windows x64 CI build", 67 | "description": "Build for Windows x64 on CI", 68 | "cacheVariables": { 69 | "CMAKE_COMPILE_WARNING_AS_ERROR": true 70 | } 71 | }, 72 | { 73 | "name": "ubuntu-x86_64", 74 | "displayName": "Ubuntu x86_64", 75 | "description": "Build for Ubuntu x86_64", 76 | "inherits": ["template"], 77 | "binaryDir": "${sourceDir}/build_x86_64", 78 | "condition": { 79 | "type": "equals", 80 | "lhs": "${hostSystemName}", 81 | "rhs": "Linux" 82 | }, 83 | "generator": "Ninja", 84 | "warnings": {"dev": true, "deprecated": true}, 85 | "cacheVariables": { 86 | "CMAKE_BUILD_TYPE": "RelWithDebInfo", 87 | "CMAKE_INSTALL_LIBDIR": "lib/CMAKE_SYSTEM_PROCESSOR-linux-gnu" 88 | } 89 | }, 90 | { 91 | "name": "ubuntu-ci-x86_64", 92 | "inherits": ["ubuntu-x86_64"], 93 | "displayName": "Ubuntu x86_64 CI build", 94 | "description": "Build for Ubuntu x86_64 on CI", 95 | "cacheVariables": { 96 | "CMAKE_BUILD_TYPE": "RelWithDebInfo", 97 | "CMAKE_COMPILE_WARNING_AS_ERROR": true, 98 | "ENABLE_CCACHE": true 99 | } 100 | } 101 | ], 102 | "buildPresets": [ 103 | { 104 | "name": "macos", 105 | "configurePreset": "macos", 106 | "displayName": "macOS Universal", 107 | "description": "macOS build for Universal architectures", 108 | "configuration": "RelWithDebInfo" 109 | }, 110 | { 111 | "name": "macos-ci", 112 | "configurePreset": "macos-ci", 113 | "displayName": "macOS Universal CI", 114 | "description": "macOS CI build for Universal architectures", 115 | "configuration": "RelWithDebInfo" 116 | }, 117 | { 118 | "name": "windows-x64", 119 | "configurePreset": "windows-x64", 120 | "displayName": "Windows x64", 121 | "description": "Windows build for x64", 122 | "configuration": "RelWithDebInfo" 123 | }, 124 | { 125 | "name": "windows-ci-x64", 126 | "configurePreset": "windows-ci-x64", 127 | "displayName": "Windows x64 CI", 128 | "description": "Windows CI build for x64 (RelWithDebInfo configuration)", 129 | "configuration": "RelWithDebInfo" 130 | }, 131 | { 132 | "name": "ubuntu-x86_64", 133 | "configurePreset": "ubuntu-x86_64", 134 | "displayName": "Ubuntu x86_64", 135 | "description": "Ubuntu build for x86_64", 136 | "configuration": "RelWithDebInfo" 137 | }, 138 | { 139 | "name": "ubuntu-ci-x86_64", 140 | "configurePreset": "ubuntu-ci-x86_64", 141 | "displayName": "Ubuntu x86_64 CI", 142 | "description": "Ubuntu CI build for x86_64", 143 | "configuration": "RelWithDebInfo" 144 | } 145 | ] 146 | } 147 | -------------------------------------------------------------------------------- /src/components/AbsoluteSlider.cpp: -------------------------------------------------------------------------------- 1 | #include "AbsoluteSlider.hpp" 2 | 3 | #include 4 | 5 | #include "moc_AbsoluteSlider.cpp" 6 | 7 | AbsoluteSlider::AbsoluteSlider(QWidget *parent) : SliderIgnoreScroll(parent) 8 | { 9 | installEventFilter(this); 10 | setMouseTracking(true); 11 | 12 | tickColor.setRgb(0x5b, 0x62, 0x73); 13 | } 14 | 15 | AbsoluteSlider::AbsoluteSlider(Qt::Orientation orientation, QWidget *parent) : SliderIgnoreScroll(orientation, parent) 16 | { 17 | installEventFilter(this); 18 | setMouseTracking(true); 19 | 20 | tickColor.setRgb(0x5b, 0x62, 0x73); 21 | } 22 | 23 | void AbsoluteSlider::mousePressEvent(QMouseEvent *event) 24 | { 25 | dragging = (event->buttons() & Qt::LeftButton || event->buttons() & Qt::MiddleButton); 26 | 27 | if (dragging) { 28 | setSliderDown(true); 29 | setValue(posToRangeValue(event)); 30 | emit AbsoluteSlider::sliderMoved(posToRangeValue(event)); 31 | } 32 | 33 | event->accept(); 34 | } 35 | 36 | void AbsoluteSlider::mouseReleaseEvent(QMouseEvent *event) 37 | { 38 | dragging = false; 39 | setSliderDown(false); 40 | event->accept(); 41 | } 42 | 43 | void AbsoluteSlider::mouseMoveEvent(QMouseEvent *event) 44 | { 45 | int val = posToRangeValue(event); 46 | 47 | if (val > maximum()) 48 | val = maximum(); 49 | else if (val < minimum()) 50 | val = minimum(); 51 | 52 | emit absoluteSliderHovered(val); 53 | 54 | if (dragging) { 55 | setValue(posToRangeValue(event)); 56 | emit AbsoluteSlider::sliderMoved(posToRangeValue(event)); 57 | } 58 | 59 | QSlider::mouseMoveEvent(event); 60 | event->accept(); 61 | } 62 | 63 | bool AbsoluteSlider::eventFilter(QObject *obj, QEvent *event) 64 | { 65 | if (event->type() == QEvent::KeyPress) { 66 | QKeyEvent *keyEvent = static_cast(event); 67 | 68 | if (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down) { 69 | return true; 70 | } 71 | } 72 | 73 | return QSlider::eventFilter(obj, event); 74 | } 75 | 76 | int AbsoluteSlider::posToRangeValue(QMouseEvent *event) 77 | { 78 | QStyleOptionSlider opt; 79 | initStyleOption(&opt); 80 | 81 | int pos; 82 | int sliderMin; 83 | int sliderMax; 84 | int handleLength; 85 | 86 | const QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); 87 | const QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); 88 | 89 | if (orientation() == Qt::Horizontal) { 90 | pos = event->pos().x(); 91 | handleLength = handle.width(); 92 | sliderMin = groove.left() + (handleLength / 2); 93 | sliderMax = groove.right() - (handleLength / 2) + 1; 94 | } else { 95 | pos = event->pos().y(); 96 | handleLength = handle.height(); 97 | sliderMin = groove.top() + (handleLength / 2); 98 | sliderMax = groove.bottom() - (handleLength / 2) + 1; 99 | } 100 | 101 | int sliderValue = style()->sliderValueFromPosition(minimum(), maximum(), pos - sliderMin, sliderMax - sliderMin, 102 | opt.upsideDown); 103 | 104 | return sliderValue; 105 | } 106 | 107 | bool AbsoluteSlider::getDisplayTicks() const 108 | { 109 | return displayTicks; 110 | } 111 | 112 | void AbsoluteSlider::setDisplayTicks(bool display) 113 | { 114 | displayTicks = display; 115 | } 116 | 117 | QColor AbsoluteSlider::getTickColor() const 118 | { 119 | return tickColor; 120 | } 121 | 122 | void AbsoluteSlider::setTickColor(QColor c) 123 | { 124 | tickColor = std::move(c); 125 | } 126 | 127 | void AbsoluteSlider::paintEvent(QPaintEvent *event) 128 | { 129 | if (!getDisplayTicks()) { 130 | QSlider::paintEvent(event); 131 | return; 132 | } 133 | 134 | QPainter painter(this); 135 | 136 | QStyleOptionSlider opt; 137 | initStyleOption(&opt); 138 | 139 | QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); 140 | QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); 141 | 142 | const bool isHorizontal = orientation() == Qt::Horizontal; 143 | 144 | const int sliderLength = isHorizontal ? groove.width() - handle.width() : groove.height() - handle.height(); 145 | const int handleSize = isHorizontal ? handle.width() : handle.height(); 146 | const int grooveSize = isHorizontal ? groove.height() : groove.width(); 147 | const int grooveStart = isHorizontal ? groove.left() : groove.top(); 148 | const int tickLinePos = isHorizontal ? groove.center().y() : groove.center().x(); 149 | const int tickLength = std::max((int)(grooveSize * 1.5) + grooveSize, 8 + grooveSize); 150 | const int tickLineStart = tickLinePos - (tickLength / 2) + 1; 151 | 152 | for (double offset = minimum(); offset <= maximum(); offset += singleStep()) { 153 | double tickPercent = (offset - minimum()) / (maximum() - minimum()); 154 | const int tickLineOffset = grooveStart + std::floor(sliderLength * tickPercent) + (handleSize / 2); 155 | 156 | const int xPos = isHorizontal ? tickLineOffset : tickLineStart; 157 | const int yPos = isHorizontal ? tickLineStart : tickLineOffset; 158 | 159 | const int tickWidth = isHorizontal ? 1 : tickLength; 160 | const int tickHeight = isHorizontal ? tickLength : 1; 161 | 162 | painter.fillRect(xPos, yPos, tickWidth, tickHeight, tickColor); 163 | } 164 | 165 | QSlider::paintEvent(event); 166 | } 167 | -------------------------------------------------------------------------------- /src/forms/MediaEdit.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MediaEdit 4 | 5 | 6 | 7 | 0 8 | 0 9 | 524 10 | 142 11 | 12 | 13 | 14 | MediaProps 15 | 16 | 17 | 18 | 6 19 | 20 | 21 | 9 22 | 23 | 24 | 9 25 | 26 | 27 | 9 28 | 29 | 30 | 9 31 | 32 | 33 | 34 | 35 | QLayout::SizeConstraint::SetFixedSize 36 | 37 | 38 | Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter 39 | 40 | 41 | 6 42 | 43 | 44 | 6 45 | 46 | 47 | 0 48 | 49 | 50 | 0 51 | 52 | 53 | 0 54 | 55 | 56 | 0 57 | 58 | 59 | 60 | 61 | Name 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 0 70 | 0 71 | 72 | 73 | 74 | 75 | 16777215 76 | 16777215 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | File 85 | 86 | 87 | 88 | 89 | 90 | 91 | QLayout::SizeConstraint::SetFixedSize 92 | 93 | 94 | 95 | 96 | 97 | 0 98 | 0 99 | 100 | 101 | 102 | 103 | 16777215 104 | 16777215 105 | 106 | 107 | 108 | true 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 0 117 | 0 118 | 119 | 120 | 121 | 122 | 16777215 123 | 16777215 124 | 125 | 126 | 127 | Browse 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | Loop 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 0 154 | 0 155 | 156 | 157 | 158 | 159 | 16777215 160 | 16777215 161 | 162 | 163 | 164 | QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /src/forms/Soundboard.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Soundboard 4 | 5 | 6 | 7 | 0 8 | 0 9 | 428 10 | 317 11 | 12 | 13 | 14 | true 15 | 16 | 17 | Soundboard 18 | 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 0 34 | 35 | 36 | 37 | 38 | QFrame::Shape::StyledPanel 39 | 40 | 41 | QFrame::Shadow::Sunken 42 | 43 | 44 | 45 | 2 46 | 47 | 48 | 0 49 | 50 | 51 | 0 52 | 53 | 54 | 0 55 | 56 | 57 | 4 58 | 59 | 60 | 61 | 62 | Qt::ContextMenuPolicy::CustomContextMenu 63 | 64 | 65 | Qt::ScrollBarPolicy::ScrollBarAlwaysOff 66 | 67 | 68 | 69 | 70 | 71 | 72 | QToolBar { background: transparent; } 73 | 74 | 75 | false 76 | 77 | 78 | 79 | 16 80 | 16 81 | 82 | 83 | 84 | false 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | :/res/images/plus.svg:/res/images/plus.svg 103 | 104 | 105 | AddSound 106 | 107 | 108 | false 109 | 110 | 111 | icon-plus 112 | 113 | 114 | 115 | 116 | false 117 | 118 | 119 | 120 | :/res/images/minus.svg:/res/images/minus.svg 121 | 122 | 123 | RemoveSound 124 | 125 | 126 | false 127 | 128 | 129 | icon-minus 130 | 131 | 132 | 133 | 134 | false 135 | 136 | 137 | 138 | :/settings/images/settings/general.svg:/settings/images/settings/general.svg 139 | 140 | 141 | EditSound 142 | 143 | 144 | false 145 | 146 | 147 | icon-gear 148 | 149 | 150 | 151 | 152 | Duplicate 153 | 154 | 155 | 156 | 157 | 158 | SceneTree 159 | QListWidget 160 |
components/SceneTree.hpp
161 |
162 | 163 | MediaControls 164 | QWidget 165 |
components/MediaControls.hpp
166 | 1 167 |
168 |
169 | 170 | 171 | 172 | 173 |
174 | -------------------------------------------------------------------------------- /.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