initial commit

This commit is contained in:
Exeldro 2024-06-13 11:58:07 +02:00
commit 687bb82eea
72 changed files with 6355 additions and 0 deletions

2
.clang-format Normal file
View File

@ -0,0 +1,2 @@
BasedOnStyle: InheritParentConfig
ColumnLimit: 132

77
.github/actions/build-plugin/action.yml vendored Normal file
View File

@ -0,0 +1,77 @@
name: 'Setup and build plugin'
description: 'Builds the plugin for specified architecture and build config.'
inputs:
target:
description: 'Build target for dependencies'
required: true
config:
description: 'Build configuration'
required: false
default: 'Release'
codesign:
description: 'Enable codesigning (macOS only)'
required: false
default: 'false'
codesignIdent:
description: 'Developer ID for application codesigning (macOS only)'
required: false
default: '-'
visualStudio:
description: 'Visual Studio version (Windows only)'
required: false
default: 'Visual Studio 16 2019'
workingDirectory:
description: 'Working directory for packaging'
required: false
default: ${{ github.workspace }}
runs:
using: 'composite'
steps:
- name: Run macOS Build
if: ${{ runner.os == 'macOS' }}
shell: zsh {0}
env:
CODESIGN_IDENT: ${{ inputs.codesignIdent }}
run: |
build_args=(
-c ${{ inputs.config }}
-t macos-${{ inputs.target }}
)
if [[ '${{ inputs.codesign }}' == 'true' ]] build_args+=(-s)
if (( ${+CI} && ${+RUNNER_DEBUG} )) build_args+=(--debug)
${{ inputs.workingDirectory }}/.github/scripts/build-macos.zsh ${build_args}
- name: Run Linux Build
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
build_args=(
-c ${{ inputs.config }}
-t linux-${{ inputs.target }}
)
if [[ -n "${CI}" && -n "${RUNNER_DEBUG}" ]]; then
build_args+=(--debug)
fi
${{ inputs.workingDirectory }}/.github/scripts/build-linux.sh "${build_args[@]}"
- name: Run Windows Build
if: ${{ runner.os == 'Windows' }}
shell: pwsh
run: |
$BuildArgs = @{
Target = '${{ inputs.target }}'
Configuration = '${{ inputs.config }}'
CMakeGenerator = '${{ inputs.visualStudio }}'
}
if ( ( Test-Path env:CI ) -and ( Test-Path env:RUNNER_DEBUG ) ) {
$BuildArgs += @{
Debug = $true
}
}
${{ inputs.workingDirectory }}/.github/scripts/Build-Windows.ps1 @BuildArgs

View File

@ -0,0 +1,99 @@
name: 'Package plugin'
description: 'Packages the plugin for specified architecture and build config.'
inputs:
target:
description: 'Build target for dependencies'
required: true
config:
description: 'Build configuration'
required: false
default: 'Release'
codesign:
description: 'Enable codesigning (macOS only)'
required: false
default: 'false'
notarize:
description: 'Enable notarization (macOS only)'
required: false
default: 'false'
codesignIdent:
description: 'Developer ID for application codesigning (macOS only)'
required: false
default: '-'
installerIdent:
description: 'Developer ID for installer package codesigning (macOS only)'
required: false
default: ''
codesignUser:
description: 'Apple ID username for notarization (macOS only)'
required: false
default: ''
codesignPass:
description: 'Apple ID password for notarization (macOS only)'
required: false
default: ''
createInstaller:
description: 'Create InnoSetup installer (Windows only)'
required: false
default: 'false'
workingDirectory:
description: 'Working directory for packaging'
required: false
default: ${{ github.workspace }}
runs:
using: 'composite'
steps:
- name: Run macOS packaging
if: ${{ runner.os == 'macOS' }}
shell: zsh {0}
env:
CODESIGN_IDENT: ${{ inputs.codesignIdent }}
CODESIGN_IDENT_INSTALLER: ${{ inputs.installerIdent }}
CODESIGN_IDENT_USER: ${{ inputs.codesignUser }}
CODESIGN_IDENT_PASS: ${{ inputs.codesignPass }}
run: |
package_args=(
-c ${{ inputs.config }}
-t macos-${{ inputs.target }}
)
if [[ '${{ inputs.codesign }}' == 'true' ]] package_args+=(-s)
if [[ '${{ inputs.notarize }}' == 'true' ]] package_args+=(-n)
if (( ${+CI} && ${+RUNNER_DEBUG} )) build_args+=(--debug)
${{ inputs.workingDirectory }}/.github/scripts/package-macos.zsh ${package_args}
- name: Run Linux packaging
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
package_args=(
-c ${{ inputs.config }}
-t linux-${{ inputs.target }}
)
if [[ -n "${CI}" && -n "${RUNNER_DEBUG}" ]]; then
build_args+=(--debug)
fi
${{ inputs.workingDirectory }}/.github/scripts/package-linux.sh "${package_args[@]}"
- name: Run Windows packaging
if: ${{ runner.os == 'Windows' }}
shell: pwsh
run: |
$PackageArgs = @{
Target = '${{ inputs.target }}'
Configuration = '${{ inputs.config }}'
}
if ( '${{ inputs.createInstaller }}' -eq 'true' ) {
$PackageArgs += @{BuildInstaller = $true}
}
if ( ( Test-Path env:CI ) -and ( Test-Path env:RUNNER_DEBUG ) ) {
$BuildArgs += @{
Debug = $true
}
}
${{ inputs.workingDirectory }}/.github/scripts/Package-Windows.ps1 @PackageArgs

9
.github/scripts/.Aptfile vendored Executable file
View File

@ -0,0 +1,9 @@
package 'cmake'
package 'ccache'
package 'curl'
package 'git'
package 'jq'
package 'ninja-build', bin: 'ninja'
package 'pkg-config'
package 'clang'
package 'clang-format-13'

6
.github/scripts/.Brewfile vendored Executable file
View File

@ -0,0 +1,6 @@
brew "ccache"
brew "coreutils"
brew "cmake"
brew "git"
brew "jq"
brew "ninja"

3
.github/scripts/.Wingetfile vendored Executable file
View File

@ -0,0 +1,3 @@
package '7zip.7zip', path: '7-zip', bin: '7z'
package 'cmake', path: 'Cmake\bin', bin: 'cmake'
package 'innosetup', path: 'Inno Setup 6', bin: 'iscc'

246
.github/scripts/.build.zsh vendored Executable file
View File

@ -0,0 +1,246 @@
#!/usr/bin/env zsh
builtin emulate -L zsh
setopt EXTENDED_GLOB
setopt PUSHD_SILENT
setopt ERR_EXIT
setopt ERR_RETURN
setopt NO_UNSET
setopt PIPE_FAIL
setopt NO_AUTO_PUSHD
setopt NO_PUSHD_IGNORE_DUPS
setopt FUNCTION_ARGZERO
## Enable for script debugging
# setopt WARN_CREATE_GLOBAL
# setopt WARN_NESTED_VAR
# setopt XTRACE
autoload -Uz is-at-least && if ! is-at-least 5.2; then
print -u2 -PR "%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."
exit 1
fi
_trap_error() {
print -u2 -PR '%F{1} ✖︎ script execution error%f'
print -PR -e "
Callstack:
${(j:\n :)funcfiletrace}
"
exit 2
}
build() {
if (( ! ${+SCRIPT_HOME} )) typeset -g SCRIPT_HOME=${ZSH_ARGZERO:A:h}
local host_os=${${(s:-:)ZSH_ARGZERO:t:r}[2]}
local target="${host_os}-${CPUTYPE}"
local project_root=${SCRIPT_HOME:A:h:h}
local buildspec_file="${project_root}/buildspec.json"
trap '_trap_error' ZERR
fpath=("${SCRIPT_HOME}/utils.zsh" ${fpath})
autoload -Uz log_info log_error log_output set_loglevel check_${host_os} setup_${host_os} setup_obs setup_ccache
if [[ ! -r ${buildspec_file} ]] {
log_error \
'No buildspec.json found. Please create a build specification for your project.' \
'A buildspec.json.template file is provided in the repository to get you started.'
return 2
}
typeset -g -a skips=()
local -i _verbosity=1
local -r _version='1.0.0'
local -r -a _valid_targets=(
macos-x86_64
macos-arm64
macos-universal
linux-x86_64
)
local -r -a _valid_configs=(Debug RelWithDebInfo Release MinSizeRel)
if [[ ${host_os} == 'macos' ]] {
local -r -a _valid_generators=(Xcode Ninja 'Unix Makefiles')
local generator="${${CI:+Ninja}:-Xcode}"
} else {
local -r -a _valid_generators=(Ninja 'Unix Makefiles')
local generator='Ninja'
}
local -r _usage="
Usage: %B${functrace[1]%:*}%b <option> [<options>]
%BOptions%b:
%F{yellow} Build configuration options%f
-----------------------------------------------------------------------------
%B-t | --target%b Specify target - default: %B%F{green}${host_os}-${CPUTYPE}%f%b
%B-c | --config%b Build configuration - default: %B%F{green}RelWithDebInfo%f%b
%B-s | --codesign%b Enable codesigning (macOS only)
%B--generator%b Specify build system to generate - default: %B%F{green}Ninja%f%b
Available generators:
- Ninja
- Unix Makefiles
- Xcode (macOS only)
%F{yellow} Output options%f
-----------------------------------------------------------------------------
%B-q | --quiet%b Quiet (error output only)
%B-v | --verbose%b Verbose (more detailed output)
%B--skip-[all|build|deps|unpack]%b Skip all|building OBS|checking for dependencies|unpacking dependencies
%B--debug%b Debug (very detailed and added output)
%F{yellow} General options%f
-----------------------------------------------------------------------------
%B-h | --help%b Print this usage help
%B-V | --version%b Print script version information"
local -a args
while (( # )) {
case ${1} {
-t|--target|-c|--config|--generator)
if (( # == 1 )) || [[ ${2:0:1} == '-' ]] {
log_error "Missing value for option %B${1}%b"
log_output ${_usage}
exit 2
}
;;
}
case ${1} {
--)
shift
args+=($@)
break
;;
-t|--target)
if (( ! ${_valid_targets[(Ie)${2}]} )) {
log_error "Invalid value %B${2}%b for option %B${1}%b"
log_output ${_usage}
exit 2
}
target=${2}
shift 2
;;
-c|--config)
if (( ! ${_valid_configs[(Ie)${2}]} )) {
log_error "Invalid value %B${2}%b for option %B${1}%b"
log_output ${_usage}
exit 2
}
BUILD_CONFIG=${2}
shift 2
;;
-s|--codesign) CODESIGN=1; shift ;;
-q|--quiet) (( _verbosity -= 1 )) || true; shift ;;
-v|--verbose) (( _verbosity += 1 )); shift ;;
-h|--help) log_output ${_usage}; exit 0 ;;
-V|--version) print -Pr "${_version}"; exit 0 ;;
--debug) _verbosity=3; shift ;;
--generator)
if (( ! ${_valid_generators[(Ie)${2}]} )) {
log_error "Invalid value %B${2}%b for option %B${1}%b"
log_output ${_usage}
exit 2
}
generator=${2}
shift 2
;;
--skip-*)
local _skip="${${(s:-:)1}[-1]}"
local _check=(all deps unpack build)
(( ${_check[(Ie)${_skip}]} )) || log_warning "Invalid skip mode %B${_skip}%b supplied"
typeset -g -a skips=(${skips} ${_skip})
shift
;;
*) log_error "Unknown option: %B${1}%b"; log_output ${_usage}; exit 2 ;;
}
}
set -- ${(@)args}
set_loglevel ${_verbosity}
check_${host_os}
setup_ccache
typeset -g QT_VERSION
typeset -g DEPLOYMENT_TARGET
typeset -g OBS_DEPS_VERSION
setup_${host_os}
local product_name
local product_version
read -r product_name product_version <<< \
"$(jq -r '. | {name, version} | join(" ")' ${buildspec_file})"
case ${host_os} {
macos)
sed -i '' \
"s/project(\(.*\) VERSION \(.*\))/project(${product_name} VERSION ${product_version})/" \
"${project_root}/CMakeLists.txt"
;;
linux)
sed -i'' \
"s/project(\(.*\) VERSION \(.*\))/project(${product_name} VERSION ${product_version})/"\
"${project_root}/CMakeLists.txt"
;;
}
setup_obs
pushd ${project_root}
if (( ! (${skips[(Ie)all]} + ${skips[(Ie)build]}) )) {
log_info "Configuring ${product_name}..."
local _plugin_deps="${project_root:h}/obs-build-dependencies/plugin-deps-${OBS_DEPS_VERSION}-qt${QT_VERSION}-${target##*-}"
local -a cmake_args=(
-DCMAKE_BUILD_TYPE=${BUILD_CONFIG:-RelWithDebInfo}
-DQT_VERSION=${QT_VERSION}
-DCMAKE_PREFIX_PATH="${_plugin_deps}"
)
if (( _loglevel == 0 )) cmake_args+=(-Wno_deprecated -Wno-dev --log-level=ERROR)
if (( _loglevel > 2 )) cmake_args+=(--debug-output)
local num_procs
case ${target} {
macos-*)
autoload -Uz read_codesign
if (( ${+CODESIGN} )) {
read_codesign
}
cmake_args+=(
-DCMAKE_FRAMEWORK_PATH="${_plugin_deps}/Frameworks"
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
-DOBS_CODESIGN_LINKER=ON
-DOBS_BUNDLE_CODESIGN_IDENTITY="${CODESIGN_IDENT:--}"
)
num_procs=$(( $(sysctl -n hw.ncpu) + 1 ))
;;
linux-*)
if (( ${+CI} )) {
cmake_args+=(-DCMAKE_INSTALL_PREFIX=/usr)
}
num_procs=$(( $(nproc) + 1 ))
;;
}
log_debug "Attempting to configure ${product_name} with CMake arguments: ${cmake_args}"
cmake -S . -B build_${target##*-} -G ${generator} ${cmake_args}
log_info "Building ${product_name}..."
local -a cmake_args=()
if (( _loglevel > 1 )) cmake_args+=(--verbose)
if [[ ${generator} == 'Unix Makefiles' ]] cmake_args+=(--parallel ${num_procs})
cmake --build build_${target##*-} --config ${BUILD_CONFIG:-RelWithDebInfo} ${cmake_args}
}
log_info "Installing ${product_name}..."
local -a cmake_args=()
if (( _loglevel > 1 )) cmake_args+=(--verbose)
cmake --install build_${target##*-} --config ${BUILD_CONFIG:-RelWithDebInfo} --prefix "${project_root}/release" ${cmake_args}
popd
}
build ${@}

192
.github/scripts/.package.zsh vendored Executable file
View File

@ -0,0 +1,192 @@
#!/usr/bin/env zsh
builtin emulate -L zsh
setopt EXTENDED_GLOB
setopt PUSHD_SILENT
setopt ERR_EXIT
setopt ERR_RETURN
setopt NO_UNSET
setopt PIPE_FAIL
setopt NO_AUTO_PUSHD
setopt NO_PUSHD_IGNORE_DUPS
setopt FUNCTION_ARGZERO
## Enable for script debugging
# setopt WARN_CREATE_GLOBAL
# setopt WARN_NESTED_VAR
# setopt XTRACE
autoload -Uz is-at-least && if ! is-at-least 5.2; then
print -u2 -PR "%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."
exit 1
fi
_trap_error() {
print -u2 -PR '%F{1} ✖︎ script execution error%f'
print -PR -e "
Callstack:
${(j:\n :)funcfiletrace}
"
exit 2
}
package() {
if (( ! ${+SCRIPT_HOME} )) typeset -g SCRIPT_HOME=${ZSH_ARGZERO:A:h}
local host_os=${${(s:-:)ZSH_ARGZERO:t:r}[2]}
local target="${host_os}-${CPUTYPE}"
local project_root=${SCRIPT_HOME:A:h:h}
local buildspec_file="${project_root}/buildspec.json"
trap '_trap_error' ZERR
fpath=("${SCRIPT_HOME}/utils.zsh" ${fpath})
autoload -Uz set_loglevel log_info log_error log_output check_${host_os}
local -i _verbosity=1
local -r _version='1.0.0'
local -r -a _valid_targets=(
macos-x86_64
macos-arm64
macos-universal
linux-x86_64
)
local -r -a _valid_configs=(Debug RelWithDebInfo Release MinSizeRel)
local -r _usage="
Usage: %B${functrace[1]%:*}%b <option> [<options>]
%BOptions%b:
%F{yellow} Package configuration options%f
-----------------------------------------------------------------------------
%B-t | --target%b Specify target - default: %B%F{green}${host_os}-${CPUTYPE}%f%b
%B-c | --config%b Build configuration - default: %B%F{green}RelWithDebInfo%f%b
%B-s | --codesign%b Enable codesigning (macOS only)
%B-n | --notarize%b Enable notarization (macOS only)
%F{yellow} Output options%f
-----------------------------------------------------------------------------
%B-q | --quiet%b Quiet (error output only)
%B-v | --verbose%b Verbose (more detailed output)
%B--debug%b Debug (very detailed and added output)
%F{yellow} General options%f
-----------------------------------------------------------------------------
%B-h | --help%b Print this usage help
%B-V | --version%b Print script version information"
local -a args
while (( # )) {
case ${1} {
-t|--target|-c|--config)
if (( # == 1 )) || [[ ${2:0:1} == '-' ]] {
log_error "Missing value for option %B${1}%b"
log_output ${_usage}
exit 2
}
;;
}
case ${1} {
--)
shift
args+=($@)
break
;;
-t|--target)
if (( ! ${_valid_targets[(Ie)${2}]} )) {
log_error "Invalid value %B${2}%b for option %B${1}%b"
log_output ${_usage}
exit 2
}
target=${2}
shift 2
;;
-c|--config)
if (( ! ${_valid_configs[(Ie)${2}]} )) {
log_error "Invalid value %B${2}%b for option %B${1}%b"
log_output ${_usage}
exit 2
}
BUILD_CONFIG=${2}
shift 2
;;
-s|--codesign) typeset -g CODESIGN=1; shift ;;
-n|--notarize) typeset -g NOTARIZE=1; typeset -g CODESIGN=1; shift ;;
-q|--quiet) (( _verbosity -= 1 )) || true; shift ;;
-v|--verbose) (( _verbosity += 1 )); shift ;;
-h|--help) log_output ${_usage}; exit 0 ;;
-V|--version) print -Pr "${_version}"; exit 0 ;;
--debug) _verbosity=3; shift ;;
*) log_error "Unknown option: %B${1}%b"; log_output ${_usage}; exit 2 ;;
}
}
set -- ${(@)args}
set_loglevel ${_verbosity}
check_${host_os}
local product_name
local product_version
read -r product_name product_version <<< \
"$(jq -r '. | {name, version} | join(" ")' ${project_root}/buildspec.json)"
if [[ ${host_os} == 'macos' ]] {
autoload -Uz check_packages read_codesign read_codesign_installer read_codesign_pass
local output_name="${product_name}-${product_version}-${host_os}-${target##*-}.pkg"
if [[ ! -d ${project_root}/release/${product_name}.plugin ]] {
log_error 'No release artifact found. Run the build script or the CMake install procedure first.'
return 2
}
if [[ ! -f ${project_root}/build_${target##*-}/installer-macos.generated.pkgproj ]] {
log_error 'Packages project file not found. Run the build script or the CMake build and install procedures first.'
return 2
}
check_packages
log_info "Packaging ${product_name}..."
pushd ${project_root}
packagesbuild \
--build-folder ${project_root}/release \
${project_root}/build_${target##*-}/installer-macos.generated.pkgproj
if (( ${+CODESIGN} )) {
read_codesign_installer
productsign \
--sign "${CODESIGN_IDENT_INSTALLER}" \
"${project_root}/release/${product_name}.pkg" \
"${project_root}/release/${output_name}"
rm "${project_root}/release/${product_name}.pkg"
} else {
mv "${project_root}/release/${product_name}.pkg" \
"${project_root}/release/${output_name}"
}
if (( ${+CODESIGN} && ${+NOTARIZE} )) {
if [[ ! -f "${project_root}/release/${output_name}" ]] {
log_error "No package for notarization found."
return 2
}
read_codesign_installer
read_codesign_pass
xcrun notarytool submit "${project_root}/release/${output_name}" \
--keychain-profile "OBS-Codesign-Password" --wait
xcrun stapler staple "${project_root}/release/${output_name}"
}
popd
} elif [[ ${host_os} == 'linux' ]] {
local -a cmake_args=()
if (( _loglevel > 1 )) cmake_args+=(--verbose)
pushd ${project_root}
cmake --build build_${target##*-} --config ${BUILD_CONFIG:-RelWithDebInfo} -t package ${cmake_args}
popd
}
}
package ${@}

101
.github/scripts/Build-Windows.ps1 vendored Executable file
View File

@ -0,0 +1,101 @@
[CmdletBinding()]
param(
[ValidateSet('Debug', 'RelWithDebInfo', 'Release', 'MinSizeRel')]
[string] $Configuration = 'RelWithDebInfo',
[ValidateSet('x86', 'x64')]
[string] $Target,
[ValidateSet('Visual Studio 17 2022', 'Visual Studio 16 2019')]
[string] $CMakeGenerator,
[switch] $SkipAll,
[switch] $SkipBuild,
[switch] $SkipDeps,
[switch] $SkipUnpack
)
$ErrorActionPreference = 'Stop'
if ( $DebugPreference -eq 'Continue' ) {
$VerbosePreference = 'Continue'
$InformationPreference = 'Continue'
}
if ( $PSVersionTable.PSVersion -lt '7.0.0' ) {
Write-Warning 'The obs-deps PowerShell build script requires PowerShell Core 7. Install or upgrade your PowerShell version: https://aka.ms/pscore6'
exit 2
}
function Build {
trap {
Pop-Location -Stack BuildTemp -ErrorAction 'SilentlyContinue'
Write-Error $_
exit 2
}
$ScriptHome = $PSScriptRoot
$ProjectRoot = Resolve-Path -Path "$PSScriptRoot/../.."
$BuildSpecFile = "${ProjectRoot}/buildspec.json"
$UtilityFunctions = Get-ChildItem -Path $PSScriptRoot/utils.pwsh/*.ps1 -Recurse
foreach($Utility in $UtilityFunctions) {
Write-Debug "Loading $($Utility.FullName)"
. $Utility.FullName
}
$BuildSpec = Get-Content -Path ${BuildSpecFile} -Raw | ConvertFrom-Json
$ProductName = $BuildSpec.name
$ProductVersion = $BuildSpec.version
$script:DepsVersion = ''
$script:QtVersion = '5'
$script:VisualStudioVersion = ''
$script:PlatformSDK = '10.0.18363.657'
Setup-Host
if ( $CmakeGenerator -eq '' ) {
$CmakeGenerator = $script:VisualStudioVersion
}
(Get-Content -Path ${ProjectRoot}/CMakeLists.txt -Raw) `
-replace "project\((.*) VERSION (.*)\)", "project(${ProductName} VERSION ${ProductVersion})" `
| Out-File -Path ${ProjectRoot}/CMakeLists.txt
Setup-Obs
Push-Location -Stack BuildTemp
if ( ! ( ( $SkipAll ) -or ( $SkipBuild ) ) ) {
Ensure-Location $ProjectRoot
$DepsPath = "plugin-deps-${script:DepsVersion}-qt${script:QtVersion}-${script:Target}"
$CmakeArgs = @(
'-G', $CmakeGenerator
"-DCMAKE_SYSTEM_VERSION=${script:PlatformSDK}"
"-DCMAKE_GENERATOR_PLATFORM=$(if (${script:Target} -eq "x86") { "Win32" } else { "x64" })"
"-DCMAKE_BUILD_TYPE=${Configuration}"
"-DCMAKE_PREFIX_PATH:PATH=$(Resolve-Path -Path "${ProjectRoot}/../obs-build-dependencies/${DepsPath}")"
"-DQT_VERSION=${script:QtVersion}"
)
Log-Debug "Attempting to configure OBS with CMake arguments: $($CmakeArgs | Out-String)"
Log-Information "Configuring ${ProductName}..."
Invoke-External cmake -S . -B build_${script:Target} @CmakeArgs
$CmakeArgs = @(
'--config', "${Configuration}"
)
if ( $VerbosePreference -eq 'Continue' ) {
$CmakeArgs+=('--verbose')
}
Log-Information "Building ${ProductName}..."
Invoke-External cmake --build "build_${script:Target}" @CmakeArgs
}
Log-Information "Install ${ProductName}..."
Invoke-External cmake --install "build_${script:Target}" --prefix "${ProjectRoot}/release" @CmakeArgs
Pop-Location -Stack BuildTemp
}
Build

92
.github/scripts/Package-Windows.ps1 vendored Executable file
View File

@ -0,0 +1,92 @@
[CmdletBinding()]
param(
[ValidateSet('Debug', 'RelWithDebInfo', 'Release', 'MinSizeRel')]
[string] $Configuration = 'RelWithDebInfo',
[ValidateSet('x86', 'x64', 'x86+x64')]
[string] $Target,
[switch] $BuildInstaller = $false
)
$ErrorActionPreference = 'Stop'
if ( $DebugPreference -eq 'Continue' ) {
$VerbosePreference = 'Continue'
$InformationPreference = 'Continue'
}
if ( $PSVersionTable.PSVersion -lt '7.0.0' ) {
Write-Warning 'The obs-deps PowerShell build script requires PowerShell Core 7. Install or upgrade your PowerShell version: https://aka.ms/pscore6'
exit 2
}
function Package {
trap {
Write-Error $_
exit 2
}
$ScriptHome = $PSScriptRoot
$ProjectRoot = Resolve-Path -Path "$PSScriptRoot/../.."
$BuildSpecFile = "${ProjectRoot}/buildspec.json"
$UtilityFunctions = Get-ChildItem -Path $PSScriptRoot/utils.pwsh/*.ps1 -Recurse
foreach( $Utility in $UtilityFunctions ) {
Write-Debug "Loading $($Utility.FullName)"
. $Utility.FullName
}
$BuildSpec = Get-Content -Path ${BuildSpecFile} -Raw | ConvertFrom-Json
$ProductName = $BuildSpec.name
$ProductVersion = $BuildSpec.version
$OutputName = "${ProductName}-${ProductVersion}-windows-${Target}"
Install-BuildDependencies -WingetFile "${ScriptHome}/.Wingetfile"
Log-Information "Packaging ${ProductName}..."
$RemoveArgs = @{
ErrorAction = 'SilentlyContinue'
Path = @(
"${ProjectRoot}/release/${ProductName}-*-windows-*.zip"
"${ProjectRoot}/release/${ProductName}-*-windows-*.exe"
)
}
Remove-Item @RemoveArgs
if ( ( $BuildInstaller ) ) {
if ( $Target -eq 'x86+x64' ) {
$IsccCandidates = Get-ChildItem -Recurse -Path '*.iss'
if ( $IsccCandidates.length -gt 0 ) {
$IsccFile = $IsccCandidates[0].FullName
} else {
$IsccFile = ''
}
} else {
$IsccFile = "${ProjectRoot}/build_${Target}/installer-Windows.generated.iss"
}
if ( ! ( Test-Path -Path $IsccFile ) ) {
throw 'InnoSetup install script not found. Run the build script or the CMake build and install procedures first.'
}
Log-Information 'Creating InnoSetup installer...'
Push-Location -Stack BuildTemp
Ensure-Location -Path "${ProjectRoot}/release"
Invoke-External iscc ${IsccFile} /O. /F"${OutputName}-Installer"
Pop-Location -Stack BuildTemp
}
$CompressArgs = @{
Path = (Get-ChildItem -Path "${ProjectRoot}/release" -Exclude "${OutputName}*.*")
CompressionLevel = 'Optimal'
DestinationPath = "${ProjectRoot}/release/${OutputName}.zip"
}
Compress-Archive -Force @CompressArgs
}
Package

13
.github/scripts/build-linux.sh vendored Executable file
View File

@ -0,0 +1,13 @@
#!/bin/sh
if ! type zsh > /dev/null 2>&1; then
echo ' => Installing script dependency Zsh.'
sudo apt-get -y update
sudo apt-get -y install zsh
fi
SCRIPT=$(readlink -f "${0}")
SCRIPT_DIR=$(dirname "${SCRIPT}")
zsh ${SCRIPT_DIR}/build-linux.zsh "${@}"

1
.github/scripts/build-linux.zsh vendored Symbolic link
View File

@ -0,0 +1 @@
.build.zsh

1
.github/scripts/build-macos.zsh vendored Symbolic link
View File

@ -0,0 +1 @@
.build.zsh

11
.github/scripts/check-changes.sh vendored Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
dirty=$(git ls-files --modified)
set +x
if [[ $dirty ]]; then
echo "================================="
echo "Files were not formatted properly"
echo "$dirty"
echo "================================="
exit 1
fi

53
.github/scripts/check-cmake.sh vendored Executable file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env bash
set -o errexit
set -o pipefail
if [ ${#} -eq 1 -a "${1}" = "VERBOSE" ]; then
VERBOSITY="-l debug"
else
VERBOSITY=""
fi
if [ "${CI}" ]; then
MODE="--check"
else
MODE="-i"
fi
# Runs the formatter in parallel on the code base.
# Return codes:
# - 1 there are files to be formatted
# - 0 everything looks fine
# Get CPU count
OS=$(uname)
NPROC=1
if [[ ${OS} = "Linux" ]] ; then
NPROC=$(nproc)
elif [[ ${OS} = "Darwin" ]] ; then
NPROC=$(sysctl -n hw.physicalcpu)
fi
# Discover clang-format
if ! type cmake-format 2> /dev/null ; then
echo "Required cmake-format not found"
exit 1
fi
find . -type d \( \
-path ./\*build\* -o \
-path ./release -o \
-path ./deps/jansson -o \
-path ./plugins/decklink/\*/decklink-sdk -o \
-path ./plugins/enc-amf -o \
-path ./plugins/mac-syphon/syphon-framework -o \
-path ./plugins/obs-outputs/ftl-sdk -o \
-path ./plugins/obs-vst -o \
-path ./plugins/obs-browser -o \
-path ./plugins/win-dshow/libdshowcapture -o \
-path ./plugins/obs-websocket/deps \
\) -prune -false -type f -o \
-name 'CMakeLists.txt' -or \
-name '*.cmake' \
| xargs -L10 -P ${NPROC} cmake-format ${MODE} ${VERBOSITY}

60
.github/scripts/check-format.sh vendored Executable file
View File

@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Original source https://github.com/Project-OSRM/osrm-backend/blob/master/scripts/format.sh
set -o errexit
set -o pipefail
set -o nounset
if [ ${#} -eq 1 ]; then
VERBOSITY="--verbose"
else
VERBOSITY=""
fi
# Runs the Clang Formatter in parallel on the code base.
# Return codes:
# - 1 there are files to be formatted
# - 0 everything looks fine
# Get CPU count
OS=$(uname)
NPROC=1
if [[ ${OS} = "Linux" ]] ; then
NPROC=$(nproc)
elif [[ ${OS} = "Darwin" ]] ; then
NPROC=$(sysctl -n hw.physicalcpu)
fi
# Discover clang-format
if type clang-format-13 2> /dev/null ; then
CLANG_FORMAT=clang-format-13
elif type clang-format 2> /dev/null ; then
# Clang format found, but need to check version
CLANG_FORMAT=clang-format
V=$(clang-format --version)
if [[ $V != *"version 13.0"* ]]; then
echo "clang-format is not 13.0 (returned ${V})"
exit 1
fi
else
echo "No appropriate clang-format found (expected clang-format-13.0.0, or clang-format)"
exit 1
fi
find . -type d \( \
-path ./\*build\* -o \
-path ./release -o \
-path ./cmake -o \
-path ./plugins/decklink/\*/decklink-sdk -o \
-path ./plugins/enc-amf -o \
-path ./plugins/mac-syphon/syphon-framework -o \
-path ./plugins/obs-outputs/ftl-sdk -o \
-path ./plugins/obs-websocket/deps \
\) -prune -false -type f -o \
-name '*.h' -or \
-name '*.hpp' -or \
-name '*.m' -or \
-name '*.mm' -or \
-name '*.c' -or \
-name '*.cpp' \
| xargs -L100 -P ${NPROC} "${CLANG_FORMAT}" ${VERBOSITY} -i -style=file -fallback-style=none

13
.github/scripts/package-linux.sh vendored Executable file
View File

@ -0,0 +1,13 @@
#!/bin/sh
if ! type zsh > /dev/null 2>&1; then
echo ' => Installing script dependency Zsh.'
sudo apt-get update
sudo apt-get install zsh
fi
SCRIPT=$(readlink -f "${0}")
SCRIPT_DIR=$(dirname "${SCRIPT}")
zsh ${SCRIPT_DIR}/package-linux.zsh "${@}"

1
.github/scripts/package-linux.zsh vendored Symbolic link
View File

@ -0,0 +1 @@
.package.zsh

1
.github/scripts/package-macos.zsh vendored Symbolic link
View File

@ -0,0 +1 @@
.package.zsh

25
.github/scripts/utils.pwsh/Check-Git.ps1 vendored Executable file
View File

@ -0,0 +1,25 @@
function Check-Git {
<#
.SYNOPSIS
Ensures available git executable on host system.
.DESCRIPTION
Checks whether a git command is available on the host system. If none is found,
Git is installed via winget.
.EXAMPLE
Check-Git
#>
if ( ! ( Test-Path function:Log-Info ) ) {
. $PSScriptRoot/Logger.ps1
}
Log-Information 'Checking for Git executable...'
if ( ! ( Get-Command git ) ) {
Log-Warning 'No Git executable found. Will try to install via winget.'
winget install git
} else {
Log-Debug "Git found at $(Get-Command git)."
Log-Status "Git found."
}
}

View File

@ -0,0 +1,29 @@
function Ensure-Location {
<#
.SYNOPSIS
Ensures current location to be set to specified directory.
.DESCRIPTION
If specified directory exists, switch to it. Otherwise create it,
then switch.
.EXAMPLE
Ensure-Location "My-Directory"
Ensure-Location -Path "Path-To-My-Directory"
#>
param(
[Parameter(Mandatory)]
[string] $Path
)
if ( ! ( Test-Path $Path ) ) {
$_Params = @{
ItemType = "Directory"
Path = ${Path}
ErrorAction = "SilentlyContinue"
}
New-Item @_Params | Set-Location
} else {
Set-Location -Path ${Path}
}
}

View File

@ -0,0 +1,70 @@
function Expand-ArchiveExt {
<#
.SYNOPSIS
Expands archive files.
.DESCRIPTION
Allows extraction of zip, 7z, gz, and xz archives.
Requires tar and 7-zip to be available on the system.
Archives ending with .zip but created using LZMA compression are
expanded using 7-zip as a fallback.
.EXAMPLE
Expand-ArchiveExt -Path <Path-To-Your-Archive>
Expand-ArchiveExt -Path <Path-To-Your-Archive> -DestinationPath <Expansion-Path>
#>
param(
[Parameter(Mandatory)]
[string] $Path,
[string] $DestinationPath = [System.IO.Path]::GetFileNameWithoutExtension($Path),
[switch] $Force
)
switch ( [System.IO.Path]::GetExtension($Path) ) {
.zip {
try {
Expand-Archive -Path $Path -DestinationPath $DestinationPath -Force:$Force
} catch {
if ( Get-Command 7z ) {
Invoke-External 7z x -y $Path "-o${DestinationPath}"
} else {
throw "Fallback utility 7-zip not found. Please install 7-zip first."
}
}
break
}
{ ( $_ -eq ".7z" ) -or ( $_ -eq ".exe" ) } {
if ( Get-Command 7z ) {
Invoke-External 7z x -y $Path "-o${DestinationPath}"
} else {
throw "Extraction utility 7-zip not found. Please install 7-zip first."
}
break
}
.gz {
try {
Invoke-External tar -x -o $DestinationPath -f $Path
} catch {
if ( Get-Command 7z ) {
Invoke-External 7z x -y $Path "-o${DestinationPath}"
} else {
throw "Fallback utility 7-zip not found. Please install 7-zip first."
}
}
break
}
.xz {
try {
Invoke-External tar -x -o $DestinationPath -f $Path
} catch {
if ( Get-Command 7z ) {
Invoke-External 7z x -y $Path "-o${DestinationPath}"
} else {
throw "Fallback utility 7-zip not found. Please install 7-zip first."
}
}
}
default {
throw "Unsupported archive extension provided."
}
}
}

View File

@ -0,0 +1,60 @@
function Install-BuildDependencies {
<#
.SYNOPSIS
Installs required build dependencies.
.DESCRIPTION
Additional packages might be needed for successful builds. This module contains additional
dependencies available for installation via winget and, if possible, adds their locations
to the environment path for future invocation.
.EXAMPLE
Install-BuildDependencies
#>
param(
[string] $WingetFile = "$PSScriptRoot/.Wingetfile"
)
if ( ! ( Test-Path function:Log-Warning ) ) {
. $PSScriptRoot/Logger.ps1
}
$Host64Bit = [System.Environment]::Is64BitOperatingSystem
$Paths = $Env:Path -split [System.IO.Path]::PathSeparator
$WingetOptions = @('install', '--accept-package-agreements', '--accept-source-agreements')
if ( $script:Quiet ) {
$WingetOptions += '--silent'
}
Get-Content $WingetFile | ForEach-Object {
$_, $Package, $_, $Path, $_, $Binary = ([regex]::Split($_, " (?=(?:[^']|'[^']*')*$)")) -replace ',', '' -replace "'",''
(${Env:ProgramFiles(x86)}, $Env:ProgramFiles) | ForEach-Object {
$Prefix = $_
$FullPath = "${Prefix}\${Path}"
if ( ( Test-Path $FullPath ) -and ! ( $Paths -contains $FullPath ) ) {
$Paths += $FullPath
$Env:Path = $Paths -join [System.IO.Path]::PathSeparator
}
}
Log-Debug "Checking for command ${Binary}"
$Found = Get-Command -ErrorAction SilentlyContinue $Binary
if ( $Found ) {
Log-Status "Found dependency ${Binary} as $($Found.Source)"
} else {
Log-Status "Installing package ${Package}"
try {
$Params = $WingetOptions + $Package
winget @Params
} catch {
throw "Error while installing winget package ${Package}: $_"
}
}
}
}

View File

@ -0,0 +1,40 @@
function Invoke-External {
<#
.SYNOPSIS
Invokes a non-PowerShell command.
.DESCRIPTION
Runs a non-PowerShell command, and captures its return code.
Throws an exception if the command returns non-zero.
.EXAMPLE
Invoke-External 7z x $MyArchive
#>
if ( $args.Count -eq 0 ) {
throw 'Invoke-External called without arguments.'
}
if ( ! ( Test-Path function:Log-Information ) ) {
. $PSScriptRoot/Logger.ps1
}
$Command = $args[0]
$CommandArgs = @()
if ( $args.Count -gt 1) {
$CommandArgs = $args[1..($args.Count - 1)]
}
$_EAP = $ErrorActionPreference
$ErrorActionPreference = "Continue"
Log-Debug "Invoke-External: ${Command} ${CommandArgs}"
& $command $commandArgs
$Result = $LASTEXITCODE
$ErrorActionPreference = $_EAP
if ( $Result -ne 0 ) {
throw "${Command} ${CommandArgs} exited with non-zero code ${Result}."
}
}

View File

@ -0,0 +1,117 @@
function Set-GitConfig {
<#
.SYNOPSIS
Sets a git config value.
.DESCRIPTION
Allows setting single or multiple config values in a PowerShell-friendly fashion.
.EXAMPLE
Set-GitConfig advice.detachedHead false
#>
if ( $args.Count -lt 2 ) {
throw 'Set-GitConfig called without required arguments <OPTION> <VALUE>.'
}
Invoke-External git config @args
}
function Invoke-GitCheckout {
<#
.SYNOPSIS
Checks out a specified git repository.
.DESCRIPTION
Wraps the git executable with PowerShell syntax to check out
a specified Git repository with a given commit hash and branch,
or a GitHub pull request ID.
.EXAMPLE
Invoke-GitCheckout -Uri "My-Repo-Uri" -Commit "My-Commit-Hash"
Invoke-GitCheckout -Uri "My-Repo-Uri" -Commit "My-Commit-Hash" -Branch "main"
Invoke-GitCheckout -Uri "My-Repo-Uri" -Commit "My-Commit-Hash" -PullRequest 250
#>
param(
[Parameter(Mandatory)]
[string] $Uri,
[Parameter(Mandatory)]
[string] $Commit,
[string] $Path,
[string] $Branch = "master",
[string] $PullRequest
)
if ( ! ( $Uri -like "*github.com*" ) -and ( $PullRequest -ne "" ) ) {
throw 'Fetching pull requests is only supported with GitHub-based repositories.'
}
if ( ! ( Test-Path function:Log-Information ) ) {
. $PSScriptRoot/Logger.ps1
}
if ( ! ( Test-Path function:Invoke-External ) ) {
. $PSScriptRoot/Invoke-External.ps1
}
$RepositoryName = [System.IO.Path]::GetFileNameWithoutExtension($Uri)
if ( $Path -eq "" ) {
$Path = "$(Get-Location | Convert-Path)\${RepositoryName}"
}
Push-Location -Stack GitCheckoutTemp
if ( Test-Path $Path/.git ) {
Write-Information "Repository ${RepositoryName} found in ${Path}"
Set-Location $Path
Set-GitConfig advice.detachedHead false
Set-GitConfig remote.origin.url $Uri
Set-GitConfig remote.origin.tapOpt --no-tags
$Ref = "+refs/heads/{0}:refs/remotes/origin/{0}" -f $Branch
Set-GitConfig --replace-all remote.origin.fetch $Ref
if ( $PullRequest -ne "" ) {
try {
Invoke-External git show-ref --quiet --verify refs/heads/pr-$PullRequest
} catch {
Invoke-External git fetch origin $("pull/{0}/head:pull-{0}" -f $PullRequest)
} finally {
Invoke-External git checkout -f "pull-${PullRequest}"
}
}
try {
$null = Invoke-External git rev-parse -q --verify "${Commit}^{commit}"
} catch {
Invoke-External git fetch origin
}
Invoke-External git checkout -f $Commit -- | Log-Information
} else {
Invoke-External git clone $Uri $Path
Set-Location $Path
Set-GitConfig advice.detachedHead false
if ( $PullRequest -ne "" ) {
$Ref = "pull/{0}/head:pull-{0}" -f $PullRequest
$Branch = "pull-${PullRequest}"
Invoke-External git fetch origin $Ref
Invoke-External git checkout $Branch
}
Invoke-External git checkout -f $Commit
}
Log-Information "Checked out commit ${Commit} on branch ${Branch}"
if ( Test-Path ${Path}/.gitmodules ) {
Invoke-External git submodule foreach --recursive git submodule sync
Invoke-External git submodule update --init --recursive
}
Pop-Location -Stack GitCheckoutTemp
}

123
.github/scripts/utils.pwsh/Logger.ps1 vendored Executable file
View File

@ -0,0 +1,123 @@
function Log-Debug {
[CmdletBinding()]
param(
[Parameter(Mandatory,ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[string[]] $Message
)
Process {
foreach($m in $Message) {
Write-Debug $m
}
}
}
function Log-Verbose {
[CmdletBinding()]
param(
[Parameter(Mandatory,ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[string[]] $Message
)
Process {
foreach($m in $Message) {
Write-Verbose $m
}
}
}
function Log-Warning {
[CmdletBinding()]
param(
[Parameter(Mandatory,ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[string[]] $Message
)
Process {
foreach($m in $Message) {
Write-Warning $m
}
}
}
function Log-Error {
[CmdletBinding()]
param(
[Parameter(Mandatory,ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[string[]] $Message
)
Process {
foreach($m in $Message) {
Write-Error $m
}
}
}
function Log-Information {
[CmdletBinding()]
param(
[Parameter(Mandatory,ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[string[]] $Message
)
Process {
if ( ! ( $script:Quiet ) ) {
$StageName = $( if ( $script:StageName -ne $null ) { $script:StageName } else { '' })
$Icon = ' =>'
foreach($m in $Message) {
Write-Host -NoNewLine -ForegroundColor Blue " ${StageName} $($Icon.PadRight(5)) "
Write-Host "${m}"
}
}
}
}
function Log-Status {
[CmdletBinding()]
param(
[Parameter(Mandatory,ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[string[]] $Message
)
Process {
if ( ! ( $script:Quiet ) ) {
$StageName = $( if ( $StageName -ne $null ) { $StageName } else { '' })
$Icon = ' >'
foreach($m in $Message) {
Write-Host -NoNewLine -ForegroundColor Green " ${StageName} $($Icon.PadRight(5)) "
Write-Host "${m}"
}
}
}
}
function Log-Output {
[CmdletBinding()]
param(
[Parameter(Mandatory,ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[string[]] $Message
)
Process {
if ( ! ( $script:Quiet ) ) {
$StageName = $( if ( $script:StageName -ne $null ) { $script:StageName } else { '' })
$Icon = ''
foreach($m in $Message) {
Write-Output " ${StageName} $($Icon.PadRight(5)) ${m}"
}
}
}
}
$Columns = (Get-Host).UI.RawUI.WindowSize.Width - 5

103
.github/scripts/utils.pwsh/Setup-Host.ps1 vendored Executable file
View File

@ -0,0 +1,103 @@
function Setup-Host {
if ( ! ( Test-Path function:Log-Output ) ) {
. $PSScriptRoot/Logger.ps1
}
if ( ! ( Test-Path function:Ensure-Location ) ) {
. $PSScriptRoot/Ensure-Location.ps1
}
if ( ! ( Test-Path function:Install-BuildDependencies ) ) {
. $PSScriptRoot/Install-BuildDependencies.ps1
}
if ( ! ( Test-Path function:Expand-ArchiveExt ) ) {
. $PSScriptRoot/Expand-ArchiveExt.ps1
}
Install-BuildDependencies -WingetFile "${ScriptHome}/.Wingetfile"
if ( $script:Target -eq '' ) { $script:Target = $script:HostArchitecture }
$script:QtVersion = $BuildSpec.platformConfig."windows-${script:Target}".qtVersion
$script:VisualStudioVersion = $BuildSpec.platformConfig."windows-${script:Target}".visualStudio
$script:PlatformSDK = $BuildSpec.platformConfig."windows-${script:Target}".platformSDK
if ( ! ( ( $script:SkipAll ) -or ( $script:SkipDeps ) ) ) {
('prebuilt', "qt${script:QtVersion}") | ForEach-Object {
$_Dependency = $_
$_Version = $BuildSpec.dependencies."${_Dependency}".version
$_BaseUrl = $BuildSpec.dependencies."${_Dependency}".baseUrl
$_Label = $BuildSpec.dependencies."${_Dependency}".label
$_Hash = $BuildSpec.dependencies."${_Dependency}".hashes."windows-${script:Target}"
if ( $BuildSpec.dependencies."${_Dependency}".PSobject.Properties.Name -contains "pdb-hashes" ) {
$_PdbHash = $BuildSpec.dependencies."${_Dependency}".'pdb-hashes'."$windows-${script:Target}"
}
if ( $_Version -eq '' ) {
throw "No ${_Dependency} spec found in ${script:BuildSpecFile}."
}
Log-Information "Setting up ${_Label}..."
Push-Location -Stack BuildTemp
Ensure-Location -Path "$(Resolve-Path -Path "${ProjectRoot}/..")/obs-build-dependencies"
switch -wildcard ( $_Dependency ) {
prebuilt {
$_Filename = "windows-deps-${_Version}-${script:Target}.zip"
$_Uri = "${_BaseUrl}/${_Version}/${_Filename}"
$_Target = "plugin-deps-${_Version}-qt${script:QtVersion}-${script:Target}"
$script:DepsVersion = ${_Version}
}
"qt*" {
$_Filename = "windows-deps-qt${script:QtVersion}-${_Version}-${script:Target}.zip"
$_Uri = "${_BaseUrl}/${_Version}/${_Filename}"
$_Target = "plugin-deps-${_Version}-qt${script:QtVersion}-${script:Target}"
}
}
if ( ! ( Test-Path -Path $_Filename ) ) {
$Params = @{
UserAgent = 'NativeHost'
Uri = $_Uri
OutFile = $_Filename
UseBasicParsing = $true
ErrorAction = 'Stop'
}
Invoke-WebRequest @Params
Log-Status "Downloaded ${_Label} for ${script:Target}."
} else {
Log-Status "Found downloaded ${_Label}."
}
$_FileHash = Get-FileHash -Path $_Filename -Algorithm SHA256
if ( $_FileHash.Hash.ToLower() -ne $_Hash ) {
throw "Checksum of downloaded ${_Label} does not match specification. Expected '${_Hash}', 'found $(${_FileHash}.Hash.ToLower())'"
}
Log-Status "Checksum of downloaded ${_Label} matches."
if ( ! ( ( $script:SkipAll ) -or ( $script:SkipUnpack ) ) ) {
Push-Location -Stack BuildTemp
Ensure-Location -Path $_Target
Expand-ArchiveExt -Path "../${_Filename}" -DestinationPath . -Force
Pop-Location -Stack BuildTemp
}
Pop-Location -Stack BuildTemp
}
}
}
function Get-HostArchitecture {
$Host64Bit = [System.Environment]::Is64BitOperatingSystem
$HostArchitecture = ('x86', 'x64')[$Host64Bit]
return $HostArchitecture
}
$script:HostArchitecture = Get-HostArchitecture

84
.github/scripts/utils.pwsh/Setup-Obs.ps1 vendored Executable file
View File

@ -0,0 +1,84 @@
function Setup-Obs {
if ( ! ( Test-Path function:Log-Output ) ) {
. $PSScriptRoot/Logger.ps1
}
if ( ! ( Test-Path function:Check-Git ) ) {
. $PSScriptRoot/Check-Git.ps1
}
Check-Git
if ( ! ( Test-Path function:Ensure-Location ) ) {
. $PSScriptRoot/Ensure-Location.ps1
}
if ( ! ( Test-Path function:Invoke-GitCheckout ) ) {
. $PSScriptRoot/Invoke-GitCheckout.ps1
}
if ( ! ( Test-Path function:Invoke-External ) ) {
. $PSScriptRoot/Invoke-External.ps1
}
Log-Information 'Setting up OBS Studio...'
$ObsVersion = $BuildSpec.dependencies.'obs-studio'.version
$ObsRepository = $BuildSpec.dependencies.'obs-studio'.repository
$ObsBranch = $BuildSpec.dependencies.'obs-studio'.branch
$ObsHash = $BuildSpec.dependencies.'obs-studio'.hash
if ( $ObsVersion -eq '' ) {
throw 'No obs-studio version found in buildspec.json.'
}
Push-Location -Stack BuildTemp
Ensure-Location -Path "$(Resolve-Path -Path "${ProjectRoot}/../")/obs-studio"
if ( ! ( ( $script:SkipAll ) -or ( $script:SkipUnpack ) ) ) {
Invoke-GitCheckout -Uri $ObsRepository -Commit $ObsHash -Path . -Branch $ObsBranch
}
if ( ! ( ( $script:SkipAll ) -or ( $script:SkipBuild ) ) ) {
Log-Information 'Configuring OBS Studio...'
$NumProcessors = (Get-CimInstance Win32_ComputerSystem).NumberOfLogicalProcessors
if ( $NumProcessors -gt 1 ) {
$env:UseMultiToolTask = $true
$env:EnforceProcessCountAcrossBuilds = $true
}
$DepsPath = "plugin-deps-${script:DepsVersion}-qt${script:QtVersion}-${script:Target}"
$CmakeArgs = @(
'-G', $CmakeGenerator
"-DCMAKE_SYSTEM_VERSION=${script:PlatformSDK}"
"-DCMAKE_GENERATOR_PLATFORM=$(if (${script:Target} -eq "x86") { "Win32" } else { "x64" })"
"-DCMAKE_BUILD_TYPE=${script:Configuration}"
"-DQT_VERSION=${script:QtVersion}"
'-DENABLE_PLUGINS=OFF'
'-DENABLE_UI=OFF'
'-DENABLE_SCRIPTING=OFF'
"-DCMAKE_INSTALL_PREFIX:PATH=$(Resolve-Path -Path "${ProjectRoot}/../obs-build-dependencies/${DepsPath}")"
"-DCMAKE_PREFIX_PATH:PATH=$(Resolve-Path -Path "${ProjectRoot}/../obs-build-dependencies/${DepsPath}")"
)
Log-Debug "Attempting to configure OBS with CMake arguments: $($CmakeArgs | Out-String)"
Log-Information "Configuring OBS..."
Invoke-External cmake -S . -B plugin_build_${script:Target} @CmakeArgs
Log-Information 'Building libobs and obs-frontend-api...'
$CmakeArgs = @(
'--config', "$( if ( $script:Configuration -eq '' ) { 'RelWithDebInfo' } else { $script:Configuration })"
)
if ( $VerbosePreference -eq 'Continue' ) {
$CmakeArgs+=('--verbose')
}
Invoke-External cmake --build plugin_build_${script:Target} @CmakeArgs -t obs-frontend-api
Invoke-External cmake --install plugin_build_${script:Target} @CmakeArgs --component obs_libraries
}
Pop-Location -Stack BuildTemp
}

36
.github/scripts/utils.zsh/check_linux vendored Executable file
View File

@ -0,0 +1,36 @@
autoload -Uz log_info log_status log_error log_debug log_warning
log_debug 'Checking for apt-get...'
if (( ! ${+commands[apt-get]} )) {
log_error 'No apt-get command found. Please install apt'
return 2
} else {
log_debug "Apt-get located at ${commands[apt-get]}"
}
local -a dependencies=("${(f)$(<${SCRIPT_HOME}/.Aptfile)}")
local -a install_list
local binary
for dependency (${dependencies}) {
local -a tokens=(${(s: :)dependency//(,|:|\')/})
if [[ ! ${tokens[1]} == 'package' ]] continue
if [[ ${#tokens} -gt 2 && ${tokens[3]} == 'bin' ]] {
binary=${tokens[4]}
} else {
binary=${tokens[2]}
}
if (( ! ${+commands[${binary}]} )) install_list+=(${tokens[2]})
}
local -a _quiet=('' '--quiet')
log_debug "List of dependencies to install: ${install_list}"
if (( ${#install_list} )) {
if (( ! ${+CI} )) log_warning 'Dependency installation via apt may require elevated privileges'
sudo apt-get -y install ${install_list} ${_quiet[(( (_loglevel == 0) + 1 ))]}
}

20
.github/scripts/utils.zsh/check_macos vendored Executable file
View File

@ -0,0 +1,20 @@
autoload -Uz is-at-least log_info log_error log_status read_codesign
local macos_version=$(sw_vers -productVersion)
log_info 'Checking macOS version...'
if ! is-at-least 11.0 "${macos_version}"; then
log_error "Minimum required macOS version is 11.0, but running on macOS ${macos_version}"
return 2
else
log_status "macOS ${macos_version} is recent"
fi
log_info 'Checking for Homebrew...'
if (( ! ${+commands[brew]} )) {
log_error 'No Homebrew command found. Please install Homebrew (https://brew.sh)'
return 2
}
brew bundle --file "${SCRIPT_HOME}/.Brewfile"
rehash

52
.github/scripts/utils.zsh/check_packages vendored Executable file
View File

@ -0,0 +1,52 @@
if (( ! ${+commands[packagesbuild]} )) {
autoload -Uz log_info log_status mkcd
if (( ! ${+commands[curl]} )) {
log_error 'curl not found. Please install curl.'
return 2
}
if (( ! ${+project_root} )) {
log_error "'project_root' not set. Please set before running ${0}."
return 2
}
local -a curl_opts=()
if (( ! ${+CI} )) {
curl_opts+=(--progress-bar)
} else {
curl_opts+=(--show-error --silent)
}
curl_opts+=(--location -O ${@})
log_info 'Installing Packages.app...'
pushd
mkcd ${project_root:h}/obs-build-dependencies
local packages_url='http://s.sudre.free.fr/Software/files/Packages_1210_1.dmg'
local packages_hash='6afdd25386295974dad8f078b8f1e41cabebd08e72d970bf92f707c7e48b16c9'
if [[ ! -f Packages_1210_1.dmg ]] {
log_status 'Download Packages.app'
curl ${curl_opts} ${packages_url}
}
local image_checksum
read -r image_checksum _ <<< "$(sha256sum Packages_1210_1.dmg)"
if [[ ${packages_hash} != ${image_checksum} ]] {
log_error "Checksum mismatch of Packages.app download.
Expected : ${packages_hash}
Actual : ${image_checksum}"
return 2
}
hdiutil attach -noverify Packages_1210_1.dmg &> /dev/null && log_status 'Packages_1210_1.dmg image mounted.'
log_info 'Installing Packages.app...'
packages_volume=$(hdiutil info -plist | grep '<string>/Volumes/Packages' | sed 's/.*<string>\(\/Volumes\/[^<]*\)<\/string>/\1/')
sudo installer -pkg "${packages_volume}/packages/Packages.pkg" -target / && rehash
hdiutil detach ${packages_volume} &> /dev/null && log_status 'Packages.dmg image unmounted.'
}

3
.github/scripts/utils.zsh/log_debug vendored Executable file
View File

@ -0,0 +1,3 @@
if (( ! ${+_loglevel} )) typeset -g _loglevel=1
if (( _loglevel > 2 )) print -PR -e -- "%F{220}DEBUG: ${@}%f"

3
.github/scripts/utils.zsh/log_error vendored Executable file
View File

@ -0,0 +1,3 @@
local icon=' ✖︎ '
print -u2 -PR "%F{1} ${icon} %f ${@}"

7
.github/scripts/utils.zsh/log_info vendored Executable file
View File

@ -0,0 +1,7 @@
if (( ! ${+_loglevel} )) typeset -g _loglevel=1
if (( _loglevel > 0 )) {
local icon=' =>'
print -PR "%F{4} ${(r:5:)icon}%f %B${@}%b"
}

7
.github/scripts/utils.zsh/log_output vendored Executable file
View File

@ -0,0 +1,7 @@
if (( ! ${+_loglevel} )) typeset -g _loglevel=1
if (( _loglevel > 0 )) {
local icon=''
print -PR " ${(r:5:)icon} ${@}"
}

7
.github/scripts/utils.zsh/log_status vendored Executable file
View File

@ -0,0 +1,7 @@
if (( ! ${+_loglevel} )) typeset -g _loglevel=1
if (( _loglevel > 0 )) {
local icon=' >'
print -PR "%F{2} ${(r:5:)icon}%f ${@}"
}

5
.github/scripts/utils.zsh/log_warning vendored Executable file
View File

@ -0,0 +1,5 @@
if (( _loglevel > 0 )) {
local icon=' =>'
print -PR "%F{3} ${(r:5:)icon} ${@}%f"
}

1
.github/scripts/utils.zsh/mkcd vendored Executable file
View File

@ -0,0 +1 @@
[[ -n ${1} ]] && mkdir -p ${1} && builtin cd ${1}

7
.github/scripts/utils.zsh/read_codesign vendored Executable file
View File

@ -0,0 +1,7 @@
autoload -Uz log_info
if (( ! ${+CODESIGN_IDENT} )) {
typeset -g CODESIGN_IDENT
log_info 'Setting up identity for application codesigning...'
read CODESIGN_IDENT'?Apple Developer Application ID: '
}

View File

@ -0,0 +1,7 @@
autoload -Uz log_info
if (( ! ${+CODESIGN_IDENT_INSTALLER} )) {
typeset -g CODESIGN_IDENT_INSTALLER
log_info 'Setting up identity for installer package codesigning...'
read CODESIGN_IDENT_INSTALLER'?Apple Developer Installer ID: '
}

33
.github/scripts/utils.zsh/read_codesign_pass vendored Executable file
View File

@ -0,0 +1,33 @@
##############################################################################
# Apple Developer credentials necessary:
#
# + Signing for distribution and notarization require an active Apple
# Developer membership
# + An Apple Development identity is needed for code signing
# (i.e. 'Apple Development: YOUR APPLE ID (PROVIDER)')
# + Your Apple developer ID is needed for notarization
# + An app-specific password is necessary for notarization from CLI
# + This password will be stored in your macOS keychain under the identifier
# 'OBS-Codesign-Password'with access Apple's 'altool' only.
##############################################################################
autoload -Uz read_codesign read_codesign_user log_info
if (( ! ${+CODESIGN_IDENT} )) {
read_codesign
}
local codesign_ident_short=$(print "${CODESIGN_IDENT}" | /usr/bin/sed -En 's/.+\((.+)\)/\1/p')
if (( ! ${+CODESIGN_IDENT_USER} )) {
read_codesign_user
}
log_info 'Setting up password for notarization keychain...'
if (( ! ${+CODESIGN_IDENT_PASS} )) {
read -s CODESIGN_IDENT_PASS'?Apple Developer ID password: '
}
print ''
log_info 'Setting up notarization keychain...'
xcrun notarytool store-credentials 'OBS-Codesign-Password' --apple-id "${CODESIGN_IDENT_USER}" --team-id "${codesign_ident_short}" --password "${CODESIGN_IDENT_PASS}"

View File

@ -0,0 +1,7 @@
autoload -Uz log_info
if (( ! ${+CODESIGN_IDENT_USER} )) {
typeset -g CODESIGN_IDENT_USER
log_info 'Setting up developer id for codesigning...'
read CODESIGN_IDENT_USER'?Apple Developer ID: '
}

17
.github/scripts/utils.zsh/set_loglevel vendored Executable file
View File

@ -0,0 +1,17 @@
autoload -Uz log_debug log_error
local -r _usage="Usage: %B${0}%b <loglevel>
Set log level, following levels are supported: 0 (quiet), 1 (normal), 2 (verbose), 3 (debug)"
if (( ! # )); then
log_error 'Called without arguments.'
log_output ${_usage}
return 2
elif (( ${1} >= 4 )); then
log_error 'Called with loglevel > 3.'
log_output ${_usage}
fi
typeset -g -i -r _loglevel=${1}
log_debug "Log level set to '${1}'"

14
.github/scripts/utils.zsh/setup_ccache vendored Executable file
View File

@ -0,0 +1,14 @@
autoload -Uz log_debug log_warning
if (( ${+commands[ccache]} )) {
log_debug "Found ccache at ${commands[ccache]}"
if (( ${+CI} )) {
ccache --set-config=cache_dir="${GITHUB_WORKSPACE:-${HOME}}/.ccache"
ccache --set-config=max_size="${CCACHE_SIZE:-500M}"
ccache --set-config=compression=true
ccache -z > /dev/null
}
} else {
log_warning "No ccache found on the system"
}

62
.github/scripts/utils.zsh/setup_linux vendored Executable file
View File

@ -0,0 +1,62 @@
autoload -Uz log_error log_status log_info mkcd
if (( ! ${+project_root} )) {
log_error "'project_root' not set. Please set before running ${0}."
return 2
}
if (( ! ${+target} )) {
log_error "'target' not set. Please set before running ${0}."
return 2
}
pushd ${project_root}
typeset -g QT_VERSION
read -r QT_VERSION <<< \
"$(jq -r --arg target "${target}" \
'.platformConfig[$target] | { qtVersion } | join(" ")' \
${project_root}/buildspec.json)"
if (( ! (${skips[(Ie)all]} + ${skips[(Ie)deps]}) )) {
log_info 'Installing obs build dependencies...'
sudo apt-get install -y \
build-essential \
libcurl4-openssl-dev \
libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev libavutil-dev \
libswresample-dev libswscale-dev \
libjansson-dev \
libx11-xcb-dev \
libgles2-mesa-dev \
libwayland-dev \
libpulse-dev
local -a _qt_packages=()
if (( QT_VERSION == 5 )) {
_qt_packages+=(
qtbase5-dev
libqt5svg5-dev
qtbase5-private-dev
libqt5x11extras5-dev
)
} elif (( QT_VERSION == 6 )) {
_qt_packages+=(
qt6-base-dev
libqt6svg6-dev
qt6-base-private-dev
)
} else {
log_error "Unsupported Qt version '${QT_VERSION}' specified."
return 2
}
sudo apt-get install -y ${_qt_packages}
}
local deps_version
read -r deps_version <<< \
"$(jq -r '.dependencies.prebuilt.version' ${buildspec_file})"
typeset -g OBS_DEPS_VERSION=${deps_version}

127
.github/scripts/utils.zsh/setup_macos vendored Executable file
View File

@ -0,0 +1,127 @@
autoload -Uz log_error log_status log_info mkcd
if (( ! ${+commands[curl]} )) {
log_error 'curl not found. Please install curl.'
return 2
}
if (( ! ${+commands[jq]} )) {
log_error 'jq not found. Please install jq.'
return 2
}
if (( ! ${+project_root} )) {
log_error "'project_root' not set. Please set before running ${0}."
return 2
}
if (( ! ${+target} )) {
log_error "'target' not set. Please set before running ${0}."
return 2
}
local -a curl_opts=()
if (( ! ${+CI} )) {
curl_opts+=(--progress-bar)
} else {
curl_opts+=(--show-error --silent)
}
curl_opts+=(--location -O ${@})
pushd ${project_root}
local _qt_version
local _deployment_target
read -r _qt_version _deployment_target <<< \
"$(jq -r --arg target "${target}" \
'.platformConfig[$target] | { qtVersion, deploymentTarget } | join (" ")' \
${buildspec_file})"
typeset -g QT_VERSION=${_qt_version}
typeset -g DEPLOYMENT_TARGET=${_deployment_target}
if (( ! (${skips[(Ie)all]} + ${skips[(Ie)deps]}) )) {
mkdir -p ${project_root:h}/obs-build-dependencies
local dependency
local deps_version
local deps_baseurl
local deps_label
local deps_hash
local _filename
local _url
local _target
local artifact_checksum
for dependency ('prebuilt' "qt${QT_VERSION}") {
IFS=';' read -r deps_version deps_baseurl deps_label deps_hash <<< \
"$(jq -r --arg dependency "${dependency}" --arg target "${target}" \
'.dependencies[$dependency] | {version, baseUrl, "label", "hash": .hashes[$target]} | join(";")' \
${buildspec_file})"
if [[ -z "${deps_version}" ]] {
log_error "No ${dependency} spec found in ${buildspec_file}."
return 2
}
log_info "Setting up ${deps_label}..."
pushd ${project_root:h}/obs-build-dependencies
case ${dependency} {
prebuilt)
_filename="macos-deps-${deps_version}-${target##*-}.tar.xz"
_url="${deps_baseurl}/${deps_version}/${_filename}"
_target="plugin-deps-${deps_version}-qt${QT_VERSION}-${target##*-}"
typeset -g OBS_DEPS_VERSION=${deps_version}
;;
qt*)
if (( ${+CI} )) {
_filename="macos-deps-qt${QT_VERSION}-${deps_version}-universal.tar.xz"
deps_hash="$(jq -r --arg dependency "${dependency}" \
'.dependencies[$dependency].hashes["macos-universal"]' \
${buildspec_file})"
} else {
_filename="macos-deps-qt${QT_VERSION}-${deps_version}-${target##*-}.tar.xz"
}
_url="${deps_baseurl}/${deps_version}/${_filename}"
_target="plugin-deps-${deps_version}-qt${QT_VERSION}-${target##*-}"
;;
}
if [[ ! -f ${_filename} ]] {
log_debug "Running curl ${curl_opts} ${_url}"
curl ${curl_opts} ${_url} && \
log_status "Downloaded ${deps_label} for ${target}."
} else {
log_status "Found downloaded ${deps_label}"
}
read -r artifact_checksum _ <<< "$(sha256sum ${_filename})"
if [[ ${deps_hash} != ${artifact_checksum} ]] {
log_error "Checksum of downloaded ${deps_label} does not match specification.
Expected : ${deps_hash}
Actual : ${artifact_checksum}"
return 2
}
log_status "Checksum of downloaded ${deps_label} matches."
if (( ! (${skips[(Ie)all]} + ${skips[(Ie)unpack]}) )) {
mkdir -p ${_target} && pushd ${_target}
XZ_OPT=-T0 tar -xzf ../${_filename} && log_status "${deps_label} extracted."
popd
}
}
popd
pushd ${project_root:h}/obs-build-dependencies
xattr -r -d com.apple.quarantine *
log_status 'Removed quarantine flag from downloaded dependencies...'
popd
} else {
local deps_version
read -r deps_version <<< \
"$(jq -r '.dependencies.prebuilt.version' ${buildspec_file})"
typeset -g OBS_DEPS_VERSION=${deps_version}
}

122
.github/scripts/utils.zsh/setup_obs vendored Executable file
View File

@ -0,0 +1,122 @@
autoload -Uz log_error log_info log_status
if (( ! ${+buildspec_file} )) {
log_error "'buildspec_file' not set. Please set before running ${0}."
return 2
}
if (( ! ${+commands[git]} )) {
log_error 'git not found. Please install git.'
return 2
}
if (( ! ${+commands[jq]} )) {
log_error 'jq not found. Please install jq.'
return 2
}
if (( ! ${+project_root} )) {
log_error "'project_root' not set. Please set before running ${0}."
return 2
}
if (( ! ${+target} )) {
log_error "'target' not set. Please set before running ${0}."
return 2
}
log_info 'Setting up OBS-Studio...'
local obs_version
local obs_repo
local obs_branch
local obs_hash
read -r obs_version obs_repo obs_branch obs_hash <<< \
"$(jq -r --arg key "obs-studio" \
'.dependencies[$key] | {version, repository, branch, hash} | join(" ")' \
${buildspec_file})"
if [[ -z ${obs_version} ]] {
log_error "No obs-studio version found in buildspec.json"
return 2
}
pushd
mkcd ${project_root:h}/obs-studio
if (( ! (${skips[(Ie)all]} + ${skips[(Ie)unpack]}) )) {
if [[ -d .git ]] {
git config advice.detachedHead false
git config remote.pluginbuild.url "${obs_repo:-https://github.com/obsproject/obs-studio.git}"
git config remote.pluginbuild.fetch "+refs/heads/${obs_branch:-master}:refs/remotes/origin/${obs_branch:-master}"
git rev-parse -q --verify "${obs_hash}^{commit}" > /dev/null || git fetch pluginbuild
git checkout ${obs_branch:-master} -B ${product_name}
git reset --hard "${obs_hash}"
log_status 'Found existing obs-studio repository.'
} else {
git clone "${obs_repo:-https://github.com/obsproject/obs-studio.git}" "${PWD}"
git config advice.detachedHead false
git checkout -f "${obs_hash}" --
git checkout ${obs_branch:-master} -b ${product_name}
log_status 'obs-studio checked out.'
}
git submodule foreach --recursive git submodule sync
git submodule update --init --recursive
}
if (( ! (${skips[(Ie)all]} + ${skips[(Ie)build]}) )) {
log_info 'Configuring obs-studio...'
local -a cmake_args=(
-DCMAKE_BUILD_TYPE=${BUILD_CONFIG:-Release}
-DQT_VERSION=${QT_VERSION}
-DENABLE_PLUGINS=OFF
-DENABLE_UI=OFF
-DENABLE_SCRIPTING=OFF
-DCMAKE_INSTALL_PREFIX="${project_root:h}/obs-build-dependencies/plugin-deps-${OBS_DEPS_VERSION}-qt${QT_VERSION}-${target##*-}"
-DCMAKE_PREFIX_PATH="${project_root:h}/obs-build-dependencies/plugin-deps-${OBS_DEPS_VERSION}-qt${QT_VERSION}-${target##*-}"
)
if (( _loglevel == 0 )) cmake_args+=(-Wno_deprecated -Wno-dev --log-level=ERROR)
if (( _loglevel > 2 )) cmake_args+=(--debug-output)
local num_procs
case ${target} {
macos-*)
autoload -Uz read_codesign
if (( ${+CODESIGN} )) {
read_codesign
}
cmake_args+=(
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
-DOBS_CODESIGN_LINKER=ON
-DOBS_BUNDLE_CODESIGN_IDENTITY="${CODESIGN_IDENT:--}"
)
num_procs=$(( $(sysctl -n hw.ncpu) + 1 ))
;;
linux-*)
cmake_args+=(
-DENABLE_PIPEWIRE=OFF
)
num_procs=$(( $(nproc) + 1 ))
;;
}
log_debug "Attempting to configure OBS with CMake arguments: ${cmake_args}"
cmake -S . -B plugin_build_${target##*-} -G ${generator} ${cmake_args}
log_info 'Building libobs and obs-frontend-api...'
local -a cmake_args=()
if (( _loglevel > 1 )) cmake_args+=(--verbose)
if [[ ${generator} == 'Unix Makefiles' ]] cmake_args+=(--parallel ${num_procs})
cmake --build plugin_build_${target##*-} --config ${BUILD_CONFIG:-Release} ${cmake_args} -t obs-frontend-api
cmake --install plugin_build_${target##*-} --config ${BUILD_CONFIG:-Release} --component obs_libraries ${cmake_args}
}
popd

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
*~
.DS_Store
/build/
/build_*/
/release/
/installer/Output/
.vscode
.idea
# ignore generated files
*.generated.*
**/.Brewfile.lock.json

100
CMakeLists.txt Normal file
View File

@ -0,0 +1,100 @@
# --- Detect if the plugin is build out of tree or not ---
if(CMAKE_PROJECT_NAME STREQUAL "obs-studio")
set(BUILD_OUT_OF_TREE OFF)
if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0)
legacy_check()
endif()
else()
set(BUILD_OUT_OF_TREE ON)
cmake_minimum_required(VERSION 3.18)
endif()
project(aitum-multistream VERSION 0.0.1)
set(PROJECT_FULL_NAME "Aitum Multistream")
# Set new UUIDs when you start to create a new plugin.
set(MACOS_PACKAGE_UUID "231194EC-716C-4AF8-8BA0-B2BC2F7582ED")
set(MACOS_INSTALLER_UUID "5FF79C7A-9B23-42BB-A4E6-2081B2E6B5FA")
add_library(${PROJECT_NAME} MODULE)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_SOURCE_DIR}/version.h)
set(CMAKE_AUTORCC ON)
target_sources(${PROJECT_NAME} PRIVATE
config-dialog.cpp
multistream.cpp
file-updater.c
resources.qrc
config-dialog.hpp
multistream.hpp
file-updater.h)
if(BUILD_OUT_OF_TREE)
find_package(libobs REQUIRED)
find_package(obs-frontend-api REQUIRED)
find_package(CURL REQUIRED)
include(cmake/ObsPluginHelpers.cmake)
find_qt(COMPONENTS Widgets COMPONENTS_LINUX Gui)
set(OBS_FRONTEND_API_NAME "obs-frontend-api")
else()
find_package(Qt6 REQUIRED COMPONENTS Core Widgets)
set(OBS_FRONTEND_API_NAME "frontend-api")
endif()
if(OS_WINDOWS)
get_filename_component(ISS_FILES_DIR "${CMAKE_BINARY_DIR}\\..\\package" ABSOLUTE)
file(TO_NATIVE_PATH "${ISS_FILES_DIR}" ISS_FILES_DIR)
get_filename_component(ISS_PACKAGE_DIR "${CMAKE_PACKAGE_PREFIX}\\.." ABSOLUTE)
file(TO_NATIVE_PATH "${ISS_PACKAGE_DIR}" ISS_PACKAGE_DIR)
get_filename_component(ISS_SOURCE_DIR "${PROJECT_SOURCE_DIR}" ABSOLUTE)
file(TO_NATIVE_PATH "${ISS_SOURCE_DIR}" ISS_SOURCE_DIR)
configure_file("installer.iss.in"
"${PROJECT_BINARY_DIR}/installer.iss"
)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/resource.rc.in ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}.rc)
target_sources(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}.rc)
elseif(OS_MACOS)
set(MACOS_BUNDLEID "tv.aitum.${PROJECT_NAME}")
set(MACOSX_PLUGIN_GUI_IDENTIFIER "${MACOS_BUNDLEID}")
set(MACOSX_PLUGIN_BUNDLE_VERSION "${PROJECT_VERSION}")
set(MACOSX_PLUGIN_SHORT_VERSION_STRING "1")
configure_file(cmake/bundle/macos/installer-macos.pkgproj.in ${CMAKE_BINARY_DIR}/installer-macos.generated.pkgproj)
target_compile_options(${PROJECT_NAME} PRIVATE -Wall)
elseif(OS_POSIX)
target_link_libraries(${PROJECT_NAME} Qt::GuiPrivate)
endif()
set_target_properties(${PROJECT_NAME} PROPERTIES AUTOMOC ON AUTOUIC ON AUTORCC ON)
target_link_libraries(${PROJECT_NAME}
OBS::${OBS_FRONTEND_API_NAME}
CURL::libcurl
Qt::Widgets
OBS::libobs)
if(BUILD_OUT_OF_TREE)
if(NOT LIB_OUT_DIR)
set(LIB_OUT_DIR "/lib/obs-plugins")
endif()
if(NOT DATA_OUT_DIR)
set(DATA_OUT_DIR "/share/obs/obs-plugins/${PROJECT_NAME}")
endif()
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
install(TARGETS ${PROJECT_NAME}
LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}/${LIB_OUT_DIR})
install(DIRECTORY data/locale data/images
DESTINATION ${CMAKE_INSTALL_PREFIX}/${DATA_OUT_DIR})
setup_plugin_target(${PROJECT_NAME})
else()
target_include_directories(${PROJECT_NAME} PRIVATE
"${CMAKE_SOURCE_DIR}/UI/obs-frontend-api")
if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0)
set_target_properties_obs(${PROJECT_NAME} PROPERTIES FOLDER "plugins/aitum" PREFIX "")
else()
set_target_properties(${PROJECT_NAME} PROPERTIES FOLDER "plugins/aitum")
setup_plugin_target(${PROJECT_NAME})
endif()
endif()

339
LICENSE Normal file
View File

@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# Aitum Multistream for OBS Studio
Plugin for [OBS Studio](https://github.com/obsproject/obs-studio) to add [![Aitum logo](media/aitum.png) Aitum](https://aitum.tv)
# Build
- In-tree build
- Build OBS Studio: https://obsproject.com/wiki/Install-Instructions
- Check out this repository to UI/frontend-plugins/aitum-multistream
- Add `add_subdirectory(aitum-multistream)` to UI/frontend-plugins/CMakeLists.txt
- Rebuild OBS Studio
- Stand-alone build
- Verify that you have development files for OBS
- Check out this repository and run `cmake -S . -B build -DBUILD_OUT_OF_TREE=On && cmake --build build`
# Translations
Please read [Translations](TRANSLATIONS.md)

7
TRANSLATIONS.md Normal file
View File

@ -0,0 +1,7 @@
# Translations
If you'd like to contribute a translation for the Aitum Multistream Plugin into your language, feel free to start a PR.
We ask that the following terms are not translated however, mostly as they are "branding" for us and thus should be consistent between languages.
- Aitum

84
buildspec.json Normal file
View File

@ -0,0 +1,84 @@
{
"dependencies": {
"obs-studio": {
"version": "29.1.0",
"repository": "https://github.com/obsproject/obs-studio.git",
"branch": "master",
"hash": "c58e511813c33e93da7637d50aa431ae0cddda0c"
},
"prebuilt": {
"version": "2023-04-12",
"baseUrl": "https://github.com/obsproject/obs-deps/releases/download",
"label": "Pre-built obs-deps",
"hashes": {
"macos-x86_64": "81120ffa33bb050c6c5fcd236e5cedfd7b80f7053fdba271fead5af20be0b5f5",
"macos-arm64": "b9bab79611774c4651d084e14259abe889d2b8d1653787a843d08cf3f0db881c",
"macos-universal": "9535c6e1ad96f7d49960251e85a245774088d48da1d602bb82f734b10219125a",
"windows-x86": "9f8582ab5891b000869d6484ea591add9fbac9f1c91b56c7b85fdfd56a261c1b",
"windows-x64": "c13a14a1acc4224b21304d97b63da4121de1ed6981297e50496fbc474abc0503",
"linux-x86_64": "056425a8a7a4a0c242ed5ab9c1eba4dd6b004386877de4304524e7bea11c0ee2"
}
},
"qt5": {
"version": "2023-04-12",
"baseUrl": "https://github.com/obsproject/obs-deps/releases/download",
"label": "Pre-built Qt5",
"hashes": {
"macos-x86_64": "3d0381a52b0e4d49967936c4357f79ac711f43564329304a6db5c90edadd2697",
"macos-arm64": "f4b32548c0530f121956bf0a9a70c36ecbbfca81073d39c396a1759baf2a05c3",
"macos-universal": "9a6cf3b9a6c9efee6ba10df649202e8075e99f3c54ae88dc9a36dbc9d7471c1e",
"windows-x64": "6488a33a474f750d5a4a268a5e20c78bb40799d99136a1b7ce3365a843cb2fd7",
"windows-x86": "a916e09b0a874036801deab2c8a7ec14fdf5d268aa5511eac5bf40727e0c4e33"
},
"pdb-hashes": {
"windows-x64": "e0e5070143fcad9311a68ce5685d8ba8f34f581ed6942b7a92d360f94ca1ba11",
"windows-x86": "36642d1052aa461964f46c17610477b0d9b9defbe2d745ccaacb85f805c1bec2"
}
},
"qt6": {
"version": "2023-04-12",
"baseUrl": "https://github.com/obsproject/obs-deps/releases/download",
"label": "Pre-built Qt6",
"hashes": {
"macos-x86_64": "2622d6ecd484a596da12b6a45b709fe563821eda558d0bbb27335c8c2f63945c",
"macos-arm64": "4f72aa1103d88a00d90c01bb3650276ffa1408e16ef40579177605483b986dc9",
"macos-universal": "eb7614544ab4f3d2c6052c797635602280ca5b028a6b987523d8484222ce45d1",
"windows-x64": "4d39364b8a8dee5aa24fcebd8440d5c22bb4551c6b440ffeacce7d61f2ed1add",
"windows-x86": "24fc03bef153a0e027c1479e42eb08097a4ea1d70a4710825be0783d0626cb0d"
},
"pdb-hashes": {
"windows-x64": "f34ee5067be19ed370268b15c53684b7b8aaa867dc800b68931df905d679e31f",
"windows-x86": "f34d1a89fc85d92913bd6c7f75ec5c28471d74db708c98161100bc8b75f8fc63"
}
}
},
"platformConfig": {
"macos-x86_64": {
"qtVersion": 6,
"deploymentTarget": "10.15"
},
"macos-arm64": {
"qtVersion": 6,
"deploymentTarget": "11.0"
},
"macos-universal": {
"qtVersion": 6,
"deploymentTarget": "10.15"
},
"windows-x64": {
"qtVersion": 6,
"visualStudio": "Visual Studio 17 2022",
"platformSDK": "10.0.20348.0"
},
"windows-x86": {
"qtVersion": 6,
"visualStudio": "Visual Studio 17 2022",
"platformSDK": "10.0.20348.0"
},
"linux-x86_64": {
"qtVersion": 6
}
},
"name": "aitum-multistream",
"version": "0.0.1"
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>${MACOSX_PLUGIN_BUNDLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${MACOSX_PLUGIN_GUI_IDENTIFIER}</string>
<key>CFBundleVersion</key>
<string>${MACOSX_PLUGIN_BUNDLE_VERSION}</string>
<key>CFBundleShortVersionString</key>
<string>${MACOSX_PLUGIN_SHORT_VERSION_STRING}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleExecutable</key>
<string>${MACOSX_PLUGIN_EXECUTABLE_NAME}</string>
<key>CFBundlePackageType</key>
<string>${MACOSX_PLUGIN_BUNDLE_TYPE}</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>LSMinimumSystemVersion</key>
<string>10.13</string>
</dict>
</plist>

View File

@ -0,0 +1,17 @@
<!--?xml version="1.0" encoding="UTF-8"?-->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- Allows @executable_path to load libaries from within the .app bundle. -->
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,920 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PACKAGES</key>
<array>
<dict>
<key>MUST-CLOSE-APPLICATION-ITEMS</key>
<array/>
<key>MUST-CLOSE-APPLICATIONS</key>
<false/>
<key>PACKAGE_FILES</key>
<dict>
<key>DEFAULT_INSTALL_LOCATION</key>
<string>/</string>
<key>HIERARCHY</key>
<dict>
<key>CHILDREN</key>
<array>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>80</integer>
<key>PATH</key>
<string>Applications</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>509</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array>
<dict>
<key>CHILDREN</key>
<array>
<dict>
<key>CHILDREN</key>
<array>
<dict>
<key>CHILDREN</key>
<array>
<dict>
<key>BUNDLE_CAN_DOWNGRADE</key>
<false/>
<key>BUNDLE_POSTINSTALL_PATH</key>
<dict>
<key>PATH_TYPE</key>
<integer>0</integer>
</dict>
<key>BUNDLE_PREINSTALL_PATH</key>
<dict>
<key>PATH_TYPE</key>
<integer>0</integer>
</dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>80</integer>
<key>PATH</key>
<string>../@RELATIVE_INSTALL_PATH@/@CMAKE_PROJECT_NAME@.plugin</string>
<key>PATH_TYPE</key>
<integer>1</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>3</integer>
<key>UID</key>
<integer>0</integer>
</dict>
</array>
<key>GID</key>
<integer>80</integer>
<key>PATH</key>
<string>plugins</string>
<key>PATH_TYPE</key>
<integer>2</integer>
<key>PERMISSIONS</key>
<integer>509</integer>
<key>TYPE</key>
<integer>2</integer>
<key>UID</key>
<integer>0</integer>
</dict>
</array>
<key>GID</key>
<integer>80</integer>
<key>PATH</key>
<string>obs-studio</string>
<key>PATH_TYPE</key>
<integer>2</integer>
<key>PERMISSIONS</key>
<integer>509</integer>
<key>TYPE</key>
<integer>2</integer>
<key>UID</key>
<integer>0</integer>
</dict>
</array>
<key>GID</key>
<integer>80</integer>
<key>PATH</key>
<string>Application Support</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>Automator</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>Documentation</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>Extensions</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>Filesystems</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>Frameworks</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>Input Methods</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>Internet Plug-Ins</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>LaunchAgents</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>LaunchDaemons</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>PreferencePanes</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>Preferences</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>80</integer>
<key>PATH</key>
<string>Printers</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>PrivilegedHelperTools</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>1005</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>QuickLook</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>QuickTime</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>Screen Savers</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>Scripts</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>Services</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>Widgets</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
</array>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>Library</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<dict>
<key>CHILDREN</key>
<array>
<dict>
<key>CHILDREN</key>
<array/>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>Shared</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>1023</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
</array>
<key>GID</key>
<integer>80</integer>
<key>PATH</key>
<string>Users</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
</array>
<key>GID</key>
<integer>0</integer>
<key>PATH</key>
<string>/</string>
<key>PATH_TYPE</key>
<integer>0</integer>
<key>PERMISSIONS</key>
<integer>493</integer>
<key>TYPE</key>
<integer>1</integer>
<key>UID</key>
<integer>0</integer>
</dict>
<key>PAYLOAD_TYPE</key>
<integer>0</integer>
<key>PRESERVE_EXTENDED_ATTRIBUTES</key>
<false/>
<key>SHOW_INVISIBLE</key>
<false/>
<key>SPLIT_FORKS</key>
<true/>
<key>TREAT_MISSING_FILES_AS_WARNING</key>
<false/>
<key>VERSION</key>
<integer>5</integer>
</dict>
<key>PACKAGE_SCRIPTS</key>
<dict>
<key>POSTINSTALL_PATH</key>
<dict>
<key>PATH_TYPE</key>
<integer>0</integer>
</dict>
<key>PREINSTALL_PATH</key>
<dict>
<key>PATH_TYPE</key>
<integer>0</integer>
</dict>
<key>RESOURCES</key>
<array/>
</dict>
<key>PACKAGE_SETTINGS</key>
<dict>
<key>AUTHENTICATION</key>
<integer>0</integer>
<key>CONCLUSION_ACTION</key>
<integer>0</integer>
<key>FOLLOW_SYMBOLIC_LINKS</key>
<false/>
<key>IDENTIFIER</key>
<string>@MACOS_BUNDLEID@</string>
<key>LOCATION</key>
<integer>0</integer>
<key>NAME</key>
<string>@CMAKE_PROJECT_NAME@</string>
<key>OVERWRITE_PERMISSIONS</key>
<false/>
<key>PAYLOAD_SIZE</key>
<integer>-1</integer>
<key>REFERENCE_PATH</key>
<string></string>
<key>RELOCATABLE</key>
<false/>
<key>USE_HFS+_COMPRESSION</key>
<false/>
<key>VERSION</key>
<string>@CMAKE_PROJECT_VERSION@</string>
</dict>
<key>TYPE</key>
<integer>0</integer>
<key>UUID</key>
<string>@MACOS_PACKAGE_UUID@</string>
</dict>
</array>
<key>PROJECT</key>
<dict>
<key>PROJECT_COMMENTS</key>
<dict>
<key>NOTES</key>
<data>
</data>
</dict>
<key>PROJECT_PRESENTATION</key>
<dict>
<key>BACKGROUND</key>
<dict>
<key>APPAREANCES</key>
<dict>
<key>DARK_AQUA</key>
<dict/>
<key>LIGHT_AQUA</key>
<dict/>
</dict>
<key>SHARED_SETTINGS_FOR_ALL_APPAREANCES</key>
<true/>
</dict>
<key>INSTALLATION TYPE</key>
<dict>
<key>HIERARCHIES</key>
<dict>
<key>INSTALLER</key>
<dict>
<key>LIST</key>
<array>
<dict>
<key>CHILDREN</key>
<array/>
<key>DESCRIPTION</key>
<array/>
<key>OPTIONS</key>
<dict>
<key>HIDDEN</key>
<false/>
<key>STATE</key>
<integer>1</integer>
</dict>
<key>PACKAGE_UUID</key>
<string>@MACOS_PACKAGE_UUID@</string>
<key>TITLE</key>
<array/>
<key>TYPE</key>
<integer>0</integer>
<key>UUID</key>
<string>@MACOS_INSTALLER_UUID@</string>
</dict>
</array>
<key>REMOVED</key>
<dict/>
</dict>
</dict>
<key>MODE</key>
<integer>0</integer>
</dict>
<key>INSTALLATION_STEPS</key>
<array>
<dict>
<key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
<string>ICPresentationViewIntroductionController</string>
<key>INSTALLER_PLUGIN</key>
<string>Introduction</string>
<key>LIST_TITLE_KEY</key>
<string>InstallerSectionTitle</string>
</dict>
<dict>
<key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
<string>ICPresentationViewReadMeController</string>
<key>INSTALLER_PLUGIN</key>
<string>ReadMe</string>
<key>LIST_TITLE_KEY</key>
<string>InstallerSectionTitle</string>
</dict>
<dict>
<key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
<string>ICPresentationViewLicenseController</string>
<key>INSTALLER_PLUGIN</key>
<string>License</string>
<key>LIST_TITLE_KEY</key>
<string>InstallerSectionTitle</string>
</dict>
<dict>
<key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
<string>ICPresentationViewDestinationSelectController</string>
<key>INSTALLER_PLUGIN</key>
<string>TargetSelect</string>
<key>LIST_TITLE_KEY</key>
<string>InstallerSectionTitle</string>
</dict>
<dict>
<key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
<string>ICPresentationViewInstallationTypeController</string>
<key>INSTALLER_PLUGIN</key>
<string>PackageSelection</string>
<key>LIST_TITLE_KEY</key>
<string>InstallerSectionTitle</string>
</dict>
<dict>
<key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
<string>ICPresentationViewInstallationController</string>
<key>INSTALLER_PLUGIN</key>
<string>Install</string>
<key>LIST_TITLE_KEY</key>
<string>InstallerSectionTitle</string>
</dict>
<dict>
<key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
<string>ICPresentationViewSummaryController</string>
<key>INSTALLER_PLUGIN</key>
<string>Summary</string>
<key>LIST_TITLE_KEY</key>
<string>InstallerSectionTitle</string>
</dict>
</array>
<key>INTRODUCTION</key>
<dict>
<key>LOCALIZATIONS</key>
<array/>
</dict>
<key>LICENSE</key>
<dict>
<key>LOCALIZATIONS</key>
<array/>
<key>MODE</key>
<integer>0</integer>
</dict>
<key>README</key>
<dict>
<key>LOCALIZATIONS</key>
<array/>
</dict>
<key>SUMMARY</key>
<dict>
<key>LOCALIZATIONS</key>
<array/>
</dict>
<key>TITLE</key>
<dict>
<key>LOCALIZATIONS</key>
<array/>
</dict>
</dict>
<key>PROJECT_REQUIREMENTS</key>
<dict>
<key>LIST</key>
<array>
<dict>
<key>BEHAVIOR</key>
<integer>3</integer>
<key>DICTIONARY</key>
<dict>
<key>IC_REQUIREMENT_OS_DISK_TYPE</key>
<integer>1</integer>
<key>IC_REQUIREMENT_OS_DISTRIBUTION_TYPE</key>
<integer>0</integer>
<key>IC_REQUIREMENT_OS_MINIMUM_VERSION</key>
<integer>101300</integer>
</dict>
<key>IC_REQUIREMENT_CHECK_TYPE</key>
<integer>0</integer>
<key>IDENTIFIER</key>
<string>fr.whitebox.Packages.requirement.os</string>
<key>MESSAGE</key>
<array/>
<key>NAME</key>
<string>Operating System</string>
<key>STATE</key>
<true/>
</dict>
</array>
<key>RESOURCES</key>
<array/>
<key>ROOT_VOLUME_ONLY</key>
<true/>
</dict>
<key>PROJECT_SETTINGS</key>
<dict>
<key>ADVANCED_OPTIONS</key>
<dict>
<key>installer-script.domains:enable_currentUserHome</key>
<integer>1</integer>
</dict>
<key>BUILD_FORMAT</key>
<integer>0</integer>
<key>BUILD_PATH</key>
<dict>
<key>PATH</key>
<string>../@RELATIVE_BUILD_PATH@</string>
<key>PATH_TYPE</key>
<integer>1</integer>
</dict>
<key>EXCLUDED_FILES</key>
<array>
<dict>
<key>PATTERNS_ARRAY</key>
<array>
<dict>
<key>REGULAR_EXPRESSION</key>
<false/>
<key>STRING</key>
<string>.DS_Store</string>
<key>TYPE</key>
<integer>0</integer>
</dict>
</array>
<key>PROTECTED</key>
<true/>
<key>PROXY_NAME</key>
<string>Remove .DS_Store files</string>
<key>PROXY_TOOLTIP</key>
<string>Remove ".DS_Store" files created by the Finder.</string>
<key>STATE</key>
<true/>
</dict>
<dict>
<key>PATTERNS_ARRAY</key>
<array>
<dict>
<key>REGULAR_EXPRESSION</key>
<false/>
<key>STRING</key>
<string>.pbdevelopment</string>
<key>TYPE</key>
<integer>0</integer>
</dict>
</array>
<key>PROTECTED</key>
<true/>
<key>PROXY_NAME</key>
<string>Remove .pbdevelopment files</string>
<key>PROXY_TOOLTIP</key>
<string>Remove ".pbdevelopment" files created by ProjectBuilder or Xcode.</string>
<key>STATE</key>
<true/>
</dict>
<dict>
<key>PATTERNS_ARRAY</key>
<array>
<dict>
<key>REGULAR_EXPRESSION</key>
<false/>
<key>STRING</key>
<string>CVS</string>
<key>TYPE</key>
<integer>1</integer>
</dict>
<dict>
<key>REGULAR_EXPRESSION</key>
<false/>
<key>STRING</key>
<string>.cvsignore</string>
<key>TYPE</key>
<integer>0</integer>
</dict>
<dict>
<key>REGULAR_EXPRESSION</key>
<false/>
<key>STRING</key>
<string>.cvspass</string>
<key>TYPE</key>
<integer>0</integer>
</dict>
<dict>
<key>REGULAR_EXPRESSION</key>
<false/>
<key>STRING</key>
<string>.svn</string>
<key>TYPE</key>
<integer>1</integer>
</dict>
<dict>
<key>REGULAR_EXPRESSION</key>
<false/>
<key>STRING</key>
<string>.git</string>
<key>TYPE</key>
<integer>1</integer>
</dict>
<dict>
<key>REGULAR_EXPRESSION</key>
<false/>
<key>STRING</key>
<string>.gitignore</string>
<key>TYPE</key>
<integer>0</integer>
</dict>
</array>
<key>PROTECTED</key>
<true/>
<key>PROXY_NAME</key>
<string>Remove SCM metadata</string>
<key>PROXY_TOOLTIP</key>
<string>Remove helper files and folders used by the CVS, SVN or Git Source Code Management systems.</string>
<key>STATE</key>
<true/>
</dict>
<dict>
<key>PATTERNS_ARRAY</key>
<array>
<dict>
<key>REGULAR_EXPRESSION</key>
<false/>
<key>STRING</key>
<string>classes.nib</string>
<key>TYPE</key>
<integer>0</integer>
</dict>
<dict>
<key>REGULAR_EXPRESSION</key>
<false/>
<key>STRING</key>
<string>designable.db</string>
<key>TYPE</key>
<integer>0</integer>
</dict>
<dict>
<key>REGULAR_EXPRESSION</key>
<false/>
<key>STRING</key>
<string>info.nib</string>
<key>TYPE</key>
<integer>0</integer>
</dict>
</array>
<key>PROTECTED</key>
<true/>
<key>PROXY_NAME</key>
<string>Optimize nib files</string>
<key>PROXY_TOOLTIP</key>
<string>Remove "classes.nib", "info.nib" and "designable.nib" files within .nib bundles.</string>
<key>STATE</key>
<true/>
</dict>
<dict>
<key>PATTERNS_ARRAY</key>
<array>
<dict>
<key>REGULAR_EXPRESSION</key>
<false/>
<key>STRING</key>
<string>Resources Disabled</string>
<key>TYPE</key>
<integer>1</integer>
</dict>
</array>
<key>PROTECTED</key>
<true/>
<key>PROXY_NAME</key>
<string>Remove Resources Disabled folders</string>
<key>PROXY_TOOLTIP</key>
<string>Remove "Resources Disabled" folders.</string>
<key>STATE</key>
<true/>
</dict>
<dict>
<key>SEPARATOR</key>
<true/>
</dict>
</array>
<key>NAME</key>
<string>@CMAKE_PROJECT_NAME@</string>
<key>PAYLOAD_ONLY</key>
<false/>
<key>TREAT_MISSING_PRESENTATION_DOCUMENTS_AS_WARNING</key>
<false/>
</dict>
</dict>
<key>TYPE</key>
<integer>0</integer>
<key>VERSION</key>
<integer>2</integer>
</dict>
</plist>

View File

@ -0,0 +1,699 @@
if(POLICY CMP0087)
cmake_policy(SET CMP0087 NEW)
endif()
set(OBS_STANDALONE_PLUGIN_DIR ${CMAKE_SOURCE_DIR}/release)
include(GNUInstallDirs)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
set(OS_MACOS ON)
set(OS_POSIX ON)
elseif(${CMAKE_SYSTEM_NAME} MATCHES "Linux|FreeBSD|OpenBSD")
set(OS_POSIX ON)
string(TOUPPER "${CMAKE_SYSTEM_NAME}" _SYSTEM_NAME_U)
set(OS_${_SYSTEM_NAME_U} ON)
elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
set(OS_WINDOWS ON)
set(OS_POSIX OFF)
endif()
# Old-Style plugin detected, find "modern" libobs variant instead and set global include directories
# to fix "bad" plugin behavior
if(DEFINED LIBOBS_INCLUDE_DIR AND NOT TARGET OBS::libobs)
message(
DEPRECATION
"You are using an outdated method of adding 'libobs' to your project. Refer to the updated wiki on how to build and export 'libobs' and use it in your plugin projects."
)
find_package(libobs REQUIRED)
if(TARGET OBS::libobs)
set_target_properties(OBS::libobs PROPERTIES IMPORTED_GLOBAL TRUE)
message(STATUS "OBS: Using modern libobs target")
add_library(libobs ALIAS OBS::libobs)
if(OS_WINDOWS)
add_library(w32-pthreads ALIAS OBS::w32-pthreads)
endif()
endif()
endif()
# Set macOS and Windows specific if default value is used
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT AND (OS_WINDOWS OR OS_MACOS))
set(CMAKE_INSTALL_PREFIX
${OBS_STANDALONE_PLUGIN_DIR}
CACHE STRING "Directory to install OBS plugin after building" FORCE)
endif()
# Set default build type to RelWithDebInfo and specify allowed alternative values
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE
"RelWithDebInfo"
CACHE STRING "OBS build type [Release, RelWithDebInfo, Debug, MinSizeRel]" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Release RelWithDebInfo Debug MinSizeRel)
endif()
# Set default Qt version to AUTO, preferring an available Qt6 with a fallback to Qt5
if(NOT QT_VERSION)
set(QT_VERSION
AUTO
CACHE STRING "OBS Qt version [AUTO, 6, 5]" FORCE)
set_property(CACHE QT_VERSION PROPERTY STRINGS AUTO 6 5)
endif()
# Macro to find best possible Qt version for use with the project:
#
# * Use QT_VERSION value as a hint for desired Qt version
# * If "AUTO" was specified, prefer Qt6 over Qt5
# * Creates versionless targets of desired component if none had been created by Qt itself (Qt
# versions < 5.15)
#
macro(find_qt)
set(multiValueArgs COMPONENTS COMPONENTS_WIN COMPONENTS_MAC COMPONENTS_LINUX)
cmake_parse_arguments(FIND_QT "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
# Do not use versionless targets in the first step to avoid Qt::Core being clobbered by later
# opportunistic find_package runs
set(QT_NO_CREATE_VERSIONLESS_TARGETS ON)
# Loop until _QT_VERSION is set or FATAL_ERROR aborts script execution early
while(NOT _QT_VERSION)
if(QT_VERSION STREQUAL AUTO AND NOT _QT_TEST_VERSION)
set(_QT_TEST_VERSION 6)
elseif(NOT QT_VERSION STREQUAL AUTO)
set(_QT_TEST_VERSION ${QT_VERSION})
endif()
find_package(
Qt${_QT_TEST_VERSION}
COMPONENTS Core
QUIET)
if(TARGET Qt${_QT_TEST_VERSION}::Core)
set(_QT_VERSION
${_QT_TEST_VERSION}
CACHE INTERNAL "")
message(STATUS "Qt version found: ${_QT_VERSION}")
unset(_QT_TEST_VERSION)
break()
elseif(QT_VERSION STREQUAL AUTO)
if(_QT_TEST_VERSION EQUAL 6)
message(WARNING "Qt6 was not found, falling back to Qt5")
set(_QT_TEST_VERSION 5)
continue()
endif()
endif()
message(FATAL_ERROR "Neither Qt6 nor Qt5 found.")
endwhile()
# Enable versionless targets for the remaining Qt components
set(QT_NO_CREATE_VERSIONLESS_TARGETS OFF)
set(_QT_COMPONENTS ${FIND_QT_COMPONENTS})
if(OS_WINDOWS)
list(APPEND _QT_COMPONENTS ${FIND_QT_COMPONENTS_WIN})
elseif(OS_MACOS)
list(APPEND _QT_COMPONENTS ${FIND_QT_COMPONENTS_MAC})
else()
list(APPEND _QT_COMPONENTS ${FIND_QT_COMPONENTS_LINUX})
endif()
find_package(
Qt${_QT_VERSION}
COMPONENTS ${_QT_COMPONENTS}
REQUIRED)
list(APPEND _QT_COMPONENTS Core)
if("Gui" IN_LIST FIND_QT_COMPONENTS_LINUX)
list(APPEND _QT_COMPONENTS "GuiPrivate")
endif()
# Check for versionless targets of each requested component and create if necessary
foreach(_COMPONENT IN LISTS _QT_COMPONENTS)
if(NOT TARGET Qt::${_COMPONENT} AND TARGET Qt${_QT_VERSION}::${_COMPONENT})
add_library(Qt::${_COMPONENT} INTERFACE IMPORTED)
set_target_properties(Qt::${_COMPONENT} PROPERTIES INTERFACE_LINK_LIBRARIES
Qt${_QT_VERSION}::${_COMPONENT})
endif()
endforeach()
endmacro()
# Set relative path variables for file configurations
file(RELATIVE_PATH RELATIVE_INSTALL_PATH ${CMAKE_SOURCE_DIR} ${CMAKE_INSTALL_PREFIX})
file(RELATIVE_PATH RELATIVE_BUILD_PATH ${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR})
if(OS_POSIX)
# Set default GCC/clang compile options:
#
# * Treat warnings as errors
# * Enable extra warnings, https://clang.llvm.org/docs/DiagnosticsReference.html#wextra
# * Warning about usage of variable length array,
# https://clang.llvm.org/docs/DiagnosticsReference.html#wvla
# * Warning about bad format specifiers,
# https://clang.llvm.org/docs/DiagnosticsReference.html#wformat
# * Warning about non-strings used as format strings,
# https://clang.llvm.org/docs/DiagnosticsReference.html#wformat-security
# * Warning about non-exhaustive switch blocks,
# https://clang.llvm.org/docs/DiagnosticsReference.html#wswitch
# * Warning about unused parameters,
# https://clang.llvm.org/docs/DiagnosticsReference.html#wunused-parameter
# * DISABLE warning about unused functions,
# https://clang.llvm.org/docs/DiagnosticsReference.html#wunused-function
# * DISABLE warning about missing field initializers,
# https://clang.llvm.org/docs/DiagnosticsReference.html#wmissing-field-initializers
# * DISABLE strict aliasing optimisations
# * C ONLY - treat implicit function declarations (use before declare) as errors,
# https://clang.llvm.org/docs/DiagnosticsReference.html#wimplicit-function-declaration
# * C ONLY - DISABLE warning about missing braces around subobject initalizers,
# https://clang.llvm.org/docs/DiagnosticsReference.html#wmissing-braces
# * C ONLY, Clang ONLY - Warning about implicit conversion of NULL to another type,
# https://clang.llvm.org/docs/DiagnosticsReference.html#wnull-conversion
# * C & C++, Clang ONLY - Disable warning about integer conversion losing precision,
# https://clang.llvm.org/docs/DiagnosticsReference.html#wshorten-64-to-32
# * C++, GCC ONLY - Warning about implicit conversion of NULL to another type
# * Enable color diagnostics on Clang (CMAKE_COLOR_DIAGNOSTICS available in CMake 3.24)
target_compile_options(
${CMAKE_PROJECT_NAME}
PRIVATE
-Werror
-Wextra
-Wvla
-Wformat
-Wformat-security
-Wswitch
-Wunused-parameter
-Wno-unused-function
-Wno-missing-field-initializers
-fno-strict-aliasing
"$<$<COMPILE_LANGUAGE:C>:-Werror-implicit-function-declaration;-Wno-missing-braces>"
"$<$<COMPILE_LANG_AND_ID:C,AppleClang,Clang>:-Wnull-conversion;-Wno-error=shorten-64-to-32;-fcolor-diagnostics>"
"$<$<COMPILE_LANG_AND_ID:CXX,AppleClang,Clang>:-Wnull-conversion;-Wno-error=shorten-64-to-32;-fcolor-diagnostics>"
"$<$<COMPILE_LANG_AND_ID:CXX,GNU>:-Wconversion-null>"
"$<$<CONFIG:DEBUG>:-DDEBUG=1;-D_DEBUG=1>")
# GCC 12.1.0 has a regression bug which trigger maybe-uninitialized warnings where there is not.
# (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105562)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL "12.1.0")
target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE -Wno-error=maybe-uninitialized)
endif()
if(NOT CCACHE_SET)
# Try to find and enable ccache
find_program(CCACHE_PROGRAM "ccache")
set(CCACHE_SUPPORT
ON
CACHE BOOL "Enable ccache support")
mark_as_advanced(CCACHE_PROGRAM)
if(CCACHE_PROGRAM AND CCACHE_SUPPORT)
set(CMAKE_CXX_COMPILER_LAUNCHER
${CCACHE_PROGRAM}
CACHE INTERNAL "")
set(CMAKE_C_COMPILER_LAUNCHER
${CCACHE_PROGRAM}
CACHE INTERNAL "")
set(CMAKE_OBJC_COMPILER_LAUNCHER
${CCACHE_PROGRAM}
CACHE INTERNAL "")
set(CMAKE_OBJCXX_COMPILER_LAUNCHER
${CCACHE_PROGRAM}
CACHE INTERNAL "")
set(CMAKE_CUDA_COMPILER_LAUNCHER
${CCACHE_PROGRAM}
CACHE INTERNAL "") # CMake 3.9+
set(CCACHE_SET
ON
CACHE INTERNAL "")
endif()
endif()
endif()
# Set required C++ standard to C++17
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Get lowercase host architecture for easier comparison
if(MSVC_CXX_ARCHITECTURE_ID)
string(TOLOWER ${MSVC_CXX_ARCHITECTURE_ID} _HOST_ARCH)
else()
string(TOLOWER ${CMAKE_SYSTEM_PROCESSOR} _HOST_ARCH)
endif()
if(_HOST_ARCH MATCHES "i[3-6]86|x86|x64|x86_64|amd64" AND NOT CMAKE_OSX_ARCHITECTURES STREQUAL
"arm64")
# Enable MMX, SSE and SSE2 on compatible host systems (assuming no cross-compile)
set(ARCH_SIMD_FLAGS -mmmx -msse -msse2)
elseif(_HOST_ARCH MATCHES "arm64|arm64e|aarch64")
# Enable available built-in SIMD support in Clang and GCC
if(CMAKE_C_COMPILER_ID MATCHES "^(Apple)?Clang|GNU" OR CMAKE_CXX_COMPILER_ID MATCHES
"^(Apple)?Clang|GNU")
include(CheckCCompilerFlag)
include(CheckCXXCompilerFlag)
check_c_compiler_flag("-fopenmp-simd" C_COMPILER_SUPPORTS_OPENMP_SIMD)
check_cxx_compiler_flag("-fopenmp-simd" CXX_COMPILER_SUPPORTS_OPENMP_SIMD)
target_compile_options(
${CMAKE_PROJECT_NAME}
PRIVATE
-DSIMDE_ENABLE_OPENMP
"$<$<AND:$<COMPILE_LANGUAGE:C>,$<BOOL:C_COMPILER_SUPPORTS_OPENMP_SIMD>>:-fopenmp-simd>"
"$<$<AND:$<COMPILE_LANGUAGE:CXX>,$<BOOL:CXX_COMPILER_SUPPORTS_OPENMP_SIMD>>:-fopenmp-simd>")
endif()
endif()
# macOS specific settings
if(OS_MACOS)
# Set macOS-specific C++ standard library
target_compile_options(
${CMAKE_PROJECT_NAME}
PRIVATE "$<$<COMPILE_LANG_AND_ID:OBJC,AppleClang,Clang>:-fcolor-diagnostics>" -stdlib=libc++)
# Set build architecture to host architecture by default
if(NOT CMAKE_OSX_ARCHITECTURES)
set(CMAKE_OSX_ARCHITECTURES
${CMAKE_HOST_SYSTEM_PROCESSOR}
CACHE STRING "Build architecture for macOS" FORCE)
endif()
set_property(CACHE CMAKE_OSX_ARCHITECTURES PROPERTY STRINGS arm64 x86_64 "arm64;x86_64")
# Set deployment target to 11.0 for Apple Silicon or 10.15 for Intel and Universal builds
if(NOT CMAKE_OSX_DEPLOYMENT_TARGET)
set(CMAKE_XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET[arch=arm64] "11.0")
set(CMAKE_XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET[arch=x86_64] "10.15")
if("${CMAKE_OSX_ARCHITECTURES}" STREQUAL "arm64")
set(_MACOS_DEPLOYMENT_TARGET "11.0")
else()
set(_MACOS_DEPLOYMENT_TARGET "10.15")
endif()
set(CMAKE_OSX_DEPLOYMENT_TARGET
${_MACOS_DEPLOYMENT_TARGET}
CACHE STRING
"Minimum macOS version to target for deployment (at runtime); newer APIs weak linked"
FORCE)
unset(_MACOS_DEPLOYMENT_TARGET)
endif()
set_property(CACHE CMAKE_OSX_DEPLOYMENT_TARGET PROPERTY STRINGS 13.0 12.0 11.0 10.15)
# Override macOS install directory
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX
${CMAKE_BINARY_DIR}/install
CACHE STRING "Directory to install OBS to after building" FORCE)
endif()
# Set up codesigning for Xcode builds with team IDs or standalone builds with developer identity
if(NOT OBS_BUNDLE_CODESIGN_TEAM)
if(NOT OBS_BUNDLE_CODESIGN_IDENTITY)
set(OBS_BUNDLE_CODESIGN_IDENTITY
"-"
CACHE STRING "OBS code signing identity for macOS" FORCE)
endif()
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY ${OBS_BUNDLE_CODESIGN_IDENTITY})
else()
# Team ID specified, warn if Xcode generator is not used and fall back to ad-hoc signing
if(NOT XCODE)
message(
WARNING
"Code signing with a team identifier is only supported with the Xcode generator. Using ad-hoc code signature instead."
)
if(NOT OBS_BUNDLE_CODESIGN_IDENTITY)
set(OBS_BUNDLE_CODESIGN_IDENTITY
"-"
CACHE STRING "OBS code signing identity for macOS" FORCE)
endif()
else()
unset(OBS_BUNDLE_CODESIGN_IDENTITY)
set_property(CACHE OBS_BUNDLE_CODESIGN_TEAM PROPERTY HELPSTRING
"OBS code signing team for macOS")
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_STYLE Automatic)
set(CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM ${OBS_BUNDLE_CODESIGN_TEAM})
endif()
endif()
# Set path to entitlements property list for codesigning. Entitlements should match the host
# binary, in this case OBS.app.
set(OBS_CODESIGN_ENTITLEMENTS
${CMAKE_SOURCE_DIR}/cmake/bundle/macos/entitlements.plist
CACHE INTERNAL "Path to codesign entitlements plist")
# Enable linker codesigning by default. Building OBS or plugins on host systems older than macOS
# 10.15 is not supported
set(OBS_CODESIGN_LINKER
ON
CACHE BOOL "Enable linker codesigning on macOS (macOS 11+ required)")
# Tell Xcode to pretend the linker signed binaries so that editing with install_name_tool
# preserves ad-hoc signatures. This option is supported by codesign on macOS 11 or higher. See
# CMake Issue 21854: https://gitlab.kitware.com/cmake/cmake/-/issues/21854
if(OBS_CODESIGN_LINKER)
set(CMAKE_XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS "-o linker-signed")
endif()
# Set default options for bundling on macOS
set(CMAKE_MACOSX_RPATH ON)
set(CMAKE_SKIP_BUILD_RPATH OFF)
set(CMAKE_BUILD_WITH_INSTALL_RPATH OFF)
set(CMAKE_INSTALL_RPATH "@executable_path/../Frameworks/")
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH OFF)
# Helper function for plugin targets (macOS version)
function(setup_plugin_target target)
# Sanity check for required bundle information
#
# * Bundle identifier
# * Bundle version
# * Short version string
if(NOT DEFINED MACOSX_PLUGIN_GUI_IDENTIFIER)
message(
FATAL_ERROR
"No 'MACOSX_PLUGIN_GUI_IDENTIFIER' set, but is required to build plugin bundles on macOS - example: 'com.yourname.pluginname'"
)
endif()
if(NOT DEFINED MACOSX_PLUGIN_BUNDLE_VERSION)
message(
FATAL_ERROR
"No 'MACOSX_PLUGIN_BUNDLE_VERSION' set, but is required to build plugin bundles on macOS - example: '25'"
)
endif()
if(NOT DEFINED MACOSX_PLUGIN_SHORT_VERSION_STRING)
message(
FATAL_ERROR
"No 'MACOSX_PLUGIN_SHORT_VERSION_STRING' set, but is required to build plugin bundles on macOS - example: '1.0.2'"
)
endif()
# Set variables for automatic property list generation
set(MACOSX_PLUGIN_BUNDLE_NAME
"${target}"
PARENT_SCOPE)
set(MACOSX_PLUGIN_BUNDLE_VERSION
"${MACOSX_PLUGIN_BUNDLE_VERSION}"
PARENT_SCOPE)
set(MACOSX_PLUGIN_SHORT_VERSION_STRING
"${MACOSX_PLUGIN_SHORT_VERSION_STRING}"
PARENT_SCOPE)
set(MACOSX_PLUGIN_EXECUTABLE_NAME
"${target}"
PARENT_SCOPE)
set(MACOSX_PLUGIN_BUNDLE_TYPE
"BNDL"
PARENT_SCOPE)
# Set installation target to install prefix root (default for bundles)
install(
TARGETS ${target}
LIBRARY DESTINATION "."
COMPONENT obs_plugins
NAMELINK_COMPONENT ${target}_Development)
if(TARGET Qt::Core)
# Framework version has changed between Qt5 (uses wrong numerical version) and Qt6 (uses
# correct alphabetical version)
if(${_QT_VERSION} EQUAL 5)
set(_QT_FW_VERSION "${QT_VERSION}")
else()
set(_QT_FW_VERSION "A")
endif()
# Set up install-time command to fix Qt library references to point into OBS.app bundle
set(_COMMAND
"${CMAKE_INSTALL_NAME_TOOL} \\
-change ${CMAKE_PREFIX_PATH}/lib/QtWidgets.framework/Versions/${QT_VERSION}/QtWidgets @rpath/QtWidgets.framework/Versions/${_QT_FW_VERSION}/QtWidgets \\
-change ${CMAKE_PREFIX_PATH}/lib/QtCore.framework/Versions/${QT_VERSION}/QtCore @rpath/QtCore.framework/Versions/${_QT_FW_VERSION}/QtCore \\
-change ${CMAKE_PREFIX_PATH}/lib/QtGui.framework/Versions/${QT_VERSION}/QtGui @rpath/QtGui.framework/Versions/${_QT_FW_VERSION}/QtGui \\
\\\"\${CMAKE_INSTALL_PREFIX}/${target}.plugin/Contents/MacOS/${target}\\\"")
install(CODE "execute_process(COMMAND /bin/sh -c \"${_COMMAND}\")" COMPONENT obs_plugins)
unset(_QT_FW_VERSION)
endif()
# Set macOS bundle properties
set_target_properties(
${target}
PROPERTIES PREFIX ""
BUNDLE ON
BUNDLE_EXTENSION "plugin"
OUTPUT_NAME ${target}
MACOSX_BUNDLE_INFO_PLIST
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/bundle/macOS/Plugin-Info.plist.in"
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${MACOSX_PLUGIN_GUI_IDENTIFIER}"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/bundle/macOS/entitlements.plist")
# If not building with Xcode, manually code-sign the plugin
if(NOT XCODE)
set(_COMMAND
"/usr/bin/codesign --force \\
--sign \\\"${OBS_BUNDLE_CODESIGN_IDENTITY}\\\" \\
--options runtime \\
--entitlements \\\"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/bundle/macOS/entitlements.plist\\\" \\
\\\"\${CMAKE_INSTALL_PREFIX}/${target}.plugin\\\"")
install(CODE "execute_process(COMMAND /bin/sh -c \"${_COMMAND}\")" COMPONENT obs_plugins)
endif()
install_bundle_resources(${target})
endfunction()
# Helper function to add resources from "data" directory as bundle resources
function(install_bundle_resources target)
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/data)
file(GLOB_RECURSE _DATA_FILES "${CMAKE_CURRENT_SOURCE_DIR}/data/*")
foreach(_DATA_FILE IN LISTS _DATA_FILES)
file(RELATIVE_PATH _RELATIVE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/data/ ${_DATA_FILE})
get_filename_component(_RELATIVE_PATH ${_RELATIVE_PATH} PATH)
target_sources(${target} PRIVATE ${_DATA_FILE})
set_source_files_properties(${_DATA_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION
Resources/${_RELATIVE_PATH})
string(REPLACE "\\" "\\\\" _GROUP_NAME ${_RELATIVE_PATH})
source_group("Resources\\${_GROUP_NAME}" FILES ${_DATA_FILE})
endforeach()
endif()
endfunction()
else()
# Check for target architecture (64bit vs 32bit)
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(_ARCH_SUFFIX 64)
else()
set(_ARCH_SUFFIX 32)
endif()
set(OBS_OUTPUT_DIR ${CMAKE_BINARY_DIR}/rundir)
# Unix specific settings
if(OS_POSIX)
# Paths to binaries and plugins differ between portable and non-portable builds on Linux
option(LINUX_PORTABLE "Build portable version (Linux)" ON)
if(NOT LINUX_PORTABLE)
set(OBS_LIBRARY_DESTINATION ${CMAKE_INSTALL_LIBDIR})
set(OBS_PLUGIN_DESTINATION ${OBS_LIBRARY_DESTINATION}/obs-plugins)
set(CMAKE_INSTALL_RPATH ${CMAKE_INSTALL_PREFIX}/lib)
set(OBS_DATA_DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/obs)
else()
set(OBS_LIBRARY_DESTINATION bin/${_ARCH_SUFFIX}bit)
set(OBS_PLUGIN_DESTINATION obs-plugins/${_ARCH_SUFFIX}bit)
set(CMAKE_INSTALL_RPATH "$ORIGIN/" "${CMAKE_INSTALL_PREFIX}/${OBS_LIBRARY_DESTINATION}")
set(OBS_DATA_DESTINATION "data")
endif()
# Setup Linux-specific CPack values for "deb" package generation
if(OS_LINUX)
set(CPACK_PACKAGE_NAME "${CMAKE_PROJECT_NAME}")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${LINUX_MAINTAINER_EMAIL}")
set(CPACK_PACKAGE_VERSION "${CMAKE_PROJECT_VERSION}")
set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-linux-x86_64")
set(CPACK_GENERATOR "DEB")
set(CPACK_DEBIAN_PACKAGE_DEPENDS
"obs-studio (>= 27.0.0), libqt5core5a (>= 5.9.0~beta), libqt5gui5 (>= 5.3.0), libqt5widgets5 (>= 5.7.0)"
)
set(CPACK_OUTPUT_FILE_PREFIX ${CMAKE_SOURCE_DIR}/release)
if(NOT LINUX_PORTABLE)
set(CPACK_SET_DESTDIR ON)
endif()
include(CPack)
endif()
# Windows specific settings
else()
set(OBS_LIBRARY_DESTINATION "bin/${_ARCH_SUFFIX}bit")
set(OBS_LIBRARY32_DESTINATION "bin/32bit")
set(OBS_LIBRARY64_DESTINATION "bin/64bit")
set(OBS_PLUGIN_DESTINATION "obs-plugins/${_ARCH_SUFFIX}bit")
set(OBS_PLUGIN32_DESTINATION "obs-plugins/32bit")
set(OBS_PLUGIN64_DESTINATION "obs-plugins/64bit")
set(OBS_DATA_DESTINATION "data")
if(MSVC)
# Set default Visual Studio CL.exe compile options.
#
# * Enable building with multiple processes,
# https://docs.microsoft.com/en-us/cpp/build/reference/mp-build-with-multiple-processes?view=msvc-170
# * Enable lint-like warnings,
# https://docs.microsoft.com/en-us/cpp/build/reference/compiler-option-warning-level?view=msvc-170
# * Enable treating all warnings as errors,
# https://docs.microsoft.com/en-us/cpp/build/reference/compiler-option-warning-level?view=msvc-170
# * RelWithDebInfo ONLY - Enable expanding of all functions not explicitly marked for no
# inlining,
# https://docs.microsoft.com/en-us/cpp/build/reference/ob-inline-function-expansion?view=msvc-170
# * Enable UNICODE support,
# https://docs.microsoft.com/en-us/windows/win32/learnwin32/working-with-strings#unicode-and-ansi-functions
# * DISABLE warnings about using POSIX function names,
# https://docs.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-3-c4996?view=msvc-170#posix-function-names
# * DISABLE warnings about unsafe CRT library functions,
# https://docs.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-3-c4996?view=msvc-170#unsafe-crt-library-functions
# * DISABLE warnings about nonstandard nameless structs/unions,
# https://docs.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-4-c4201?view=msvc-170
target_compile_options(
${CMAKE_PROJECT_NAME}
PRIVATE /MP
/W3
/WX
/wd4201
"$<$<CONFIG:RELWITHDEBINFO>:/Ob2>"
"$<$<CONFIG:DEBUG>:/DDEBUG=1;/D_DEBUG=1>"
/DUNICODE
/D_UNICODE
/D_CRT_SECURE_NO_WARNINGS
/D_CRT_NONSTDC_NO_WARNINGS)
# Set default Visual Studio linker options.
#
# * Enable removal of functions and data that are never used,
# https://docs.microsoft.com/en-us/cpp/build/reference/opt-optimizations?view=msvc-170
# * Enable treating all warnings as errors,
# https://docs.microsoft.com/en-us/cpp/build/reference/wx-treat-linker-warnings-as-errors?view=msvc-170
# * x64 ONLY - DISABLE creation of table of safe exception handlers,
# https://docs.microsoft.com/en-us/cpp/build/reference/safeseh-image-has-safe-exception-handlers?view=msvc-170
# * Debug ONLY - DISABLE incremental linking,
# https://docs.microsoft.com/en-us/cpp/build/reference/incremental-link-incrementally?view=msvc-170
# * RelWithDebInfo ONLY - Disable incremental linking, but enable COMDAT folding,
# https://docs.microsoft.com/en-us/cpp/build/reference/opt-optimizations?view=msvc-170
target_link_options(
${CMAKE_PROJECT_NAME}
PRIVATE
"LINKER:/OPT:REF"
"LINKER:/WX"
"$<$<NOT:$<EQUAL:${CMAKE_SIZEOF_VOID_P},8>>:LINKER\:/SAFESEH\:NO>"
"$<$<CONFIG:DEBUG>:LINKER\:/INCREMENTAL\:NO>"
"$<$<CONFIG:RELWITHDEBINFO>:LINKER\:/INCREMENTAL\:NO;/OPT\:ICF>")
endif()
endif()
# Helper function for plugin targets (Windows and Linux version)
function(setup_plugin_target target)
# Set prefix to empty string to avoid automatic naming of generated library, i.e.
# "lib<YOUR_PLUGIN_NAME>"
set_target_properties(${target} PROPERTIES PREFIX "")
# Set install directories
install(
TARGETS ${target}
RUNTIME DESTINATION "${OBS_PLUGIN_DESTINATION}" COMPONENT ${target}_Runtime
LIBRARY DESTINATION "${OBS_PLUGIN_DESTINATION}"
COMPONENT ${target}_Runtime
NAMELINK_COMPONENT ${target}_Development)
# Set rundir install directory
install(
FILES $<TARGET_FILE:${target}>
DESTINATION $<CONFIG>/${OBS_PLUGIN_DESTINATION}
COMPONENT obs_rundir
EXCLUDE_FROM_ALL)
if(OS_WINDOWS)
# Set install directory for optional PDB symbol files
install(
FILES $<TARGET_PDB_FILE:${target}>
CONFIGURATIONS "RelWithDebInfo" "Debug"
DESTINATION ${OBS_PLUGIN_DESTINATION}
COMPONENT ${target}_Runtime
OPTIONAL)
# Set rundir install directory for optional PDB symbol files
install(
FILES $<TARGET_PDB_FILE:${target}>
CONFIGURATIONS "RelWithDebInfo" "Debug"
DESTINATION $<CONFIG>/${OBS_PLUGIN_DESTINATION}
COMPONENT obs_rundir
OPTIONAL EXCLUDE_FROM_ALL)
endif()
# Add resources from data directory
setup_target_resources(${target} obs-plugins/${target})
# Set up plugin for testing in available OBS build on Windows
if(OS_WINDOWS AND DEFINED OBS_BUILD_DIR)
setup_target_for_testing(${target} obs-plugins/${target})
endif()
# Custom command to install generated plugin into rundir
add_custom_command(
TARGET ${target}
POST_BUILD
COMMAND
"${CMAKE_COMMAND}" -DCMAKE_INSTALL_PREFIX=${OBS_OUTPUT_DIR}
-DCMAKE_INSTALL_COMPONENT=obs_rundir -DCMAKE_INSTALL_CONFIG_NAME=$<CONFIG> -P
${CMAKE_CURRENT_BINARY_DIR}/cmake_install.cmake
COMMENT "Installing to plugin rundir"
VERBATIM)
endfunction()
# Helper function to add resources from "data" directory
function(setup_target_resources target destination)
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/data)
install(
DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data/
DESTINATION ${OBS_DATA_DESTINATION}/${destination}
USE_SOURCE_PERMISSIONS
COMPONENT obs_plugins)
install(
DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data
DESTINATION $<CONFIG>/${OBS_DATA_DESTINATION}/${destination}
USE_SOURCE_PERMISSIONS
COMPONENT obs_rundir
EXCLUDE_FROM_ALL)
endif()
endfunction()
if(OS_WINDOWS)
# Additional Windows-only helper function to copy plugin to existing OBS development directory:
#
# Copies plugin with associated PDB symbol files as well as contents of data directory into the
# OBS rundir as specified by "OBS_BUILD_DIR".
function(setup_target_for_testing target destination)
install(
FILES $<TARGET_FILE:${target}>
DESTINATION $<CONFIG>/${OBS_PLUGIN_DESTINATION}
COMPONENT obs_testing
EXCLUDE_FROM_ALL)
install(
FILES $<TARGET_PDB_FILE:${target}>
CONFIGURATIONS "RelWithDebInfo" "Debug"
DESTINATION $<CONFIG>/${OBS_PLUGIN_DESTINATION}
COMPONENT obs_testing
OPTIONAL EXCLUDE_FROM_ALL)
install(
DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data/
DESTINATION $<CONFIG>/${OBS_DATA_DESTINATION}/${destination}
USE_SOURCE_PERMISSIONS
COMPONENT obs_testing
EXCLUDE_FROM_ALL)
add_custom_command(
TARGET ${target}
POST_BUILD
COMMAND
"${CMAKE_COMMAND}" -DCMAKE_INSTALL_PREFIX=${OBS_BUILD_DIR}/rundir
-DCMAKE_INSTALL_COMPONENT=obs_testing -DCMAKE_INSTALL_CONFIG_NAME=$<CONFIG> -P
${CMAKE_CURRENT_BINARY_DIR}/cmake_install.cmake
COMMENT "Installing to OBS test directory"
VERBATIM)
endfunction()
endif()
endif()

800
config-dialog.cpp Normal file
View File

@ -0,0 +1,800 @@
#include "config-dialog.hpp"
#include <QCheckBox>
#include <QComboBox>
#include <QFileDialog>
#include <QFormLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QPushButton>
#include <QScrollArea>
#include <QSpinBox>
#include <QStackedWidget>
#include <QTextEdit>
#include <QRadioButton>
#include <QPlainTextEdit>
#include <QCompleter>
#include <QDesktopServices>
#include <QUrl>
#include "obs-module.h"
#include "version.h"
#include <util/dstr.h>
OBSBasicSettings::OBSBasicSettings(QMainWindow *parent) : QDialog(parent)
{
setMinimumWidth(983);
setMinimumHeight(480);
setWindowTitle(obs_module_text("AitumMultistreamSettings"));
setSizeGripEnabled(true);
const auto main_window = static_cast<QMainWindow *>(obs_frontend_get_main_window());
listWidget = new QListWidget(this);
listWidget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding);
listWidget->setMaximumWidth(180);
QListWidgetItem *listwidgetitem = new QListWidgetItem(listWidget);
listwidgetitem->setIcon(QIcon(QString::fromUtf8(":/settings/images/settings/general.svg")));
//listwidgetitem->setProperty("themeID", QVariant(QString::fromUtf8("configIconSmall")));
//cogsIcon
listwidgetitem->setText(QString::fromUtf8(obs_module_text("General")));
listwidgetitem = new QListWidgetItem(listWidget);
listwidgetitem->setIcon(QIcon(QString::fromUtf8(":/settings/images/settings/stream.svg")));
listwidgetitem->setText(QString::fromUtf8(obs_module_text("MainCanvas")));
//listwidgetitem = new QListWidgetItem(listWidget);
//listwidgetitem->setIcon(QIcon(QString::fromUtf8(":/settings/images/settings/stream.svg")));
//listwidgetitem->setIcon(main_window->property("defaultIcon").value<QIcon>());
//listwidgetitem->setText(QString::fromUtf8(obs_module_text("Vertical outputs")));
listwidgetitem = new QListWidgetItem(listWidget);
listwidgetitem->setIcon(main_window->property("defaultIcon").value<QIcon>());
listwidgetitem->setText(QString::fromUtf8(obs_module_text("SetupTroubleshooter")));
listwidgetitem = new QListWidgetItem(listWidget);
listwidgetitem->setIcon(main_window->property("defaultIcon").value<QIcon>());
listwidgetitem->setText(QString::fromUtf8(obs_module_text("Help")));
listWidget->setCurrentRow(0);
listWidget->setSpacing(1);
auto settingsPages = new QStackedWidget;
settingsPages->setContentsMargins(0, 0, 0, 0);
settingsPages->setFrameShape(QFrame::NoFrame);
settingsPages->setLineWidth(0);
QWidget *generalPage = new QWidget;
QScrollArea *scrollArea = new QScrollArea;
scrollArea->setWidget(generalPage);
scrollArea->setWidgetResizable(true);
scrollArea->setLineWidth(0);
scrollArea->setFrameShape(QFrame::NoFrame);
settingsPages->addWidget(scrollArea);
auto mainOutputsPage = new QGroupBox;
mainOutputsPage->setStyleSheet(QString("QGroupBox{ padding-top: 4px;}"));
mainOutputsPage->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
scrollArea = new QScrollArea;
scrollArea->setWidget(mainOutputsPage);
scrollArea->setWidgetResizable(true);
scrollArea->setLineWidth(0);
scrollArea->setFrameShape(QFrame::NoFrame);
settingsPages->addWidget(scrollArea);
/*
auto verticalOutputsPage = new QWidget;
scrollArea = new QScrollArea;
scrollArea->setWidget(verticalOutputsPage);
scrollArea->setWidgetResizable(true);
scrollArea->setLineWidth(0);
scrollArea->setFrameShape(QFrame::NoFrame);
settingsPages->addWidget(scrollArea);*/
auto troubleshooterPage = new QWidget;
scrollArea = new QScrollArea;
scrollArea->setWidget(troubleshooterPage);
scrollArea->setWidgetResizable(true);
scrollArea->setLineWidth(0);
scrollArea->setFrameShape(QFrame::NoFrame);
settingsPages->addWidget(scrollArea);
auto helpPage = new QWidget;
scrollArea = new QScrollArea;
scrollArea->setWidget(helpPage);
scrollArea->setWidgetResizable(true);
scrollArea->setLineWidth(0);
scrollArea->setFrameShape(QFrame::NoFrame);
settingsPages->addWidget(scrollArea);
//mainOutputsPage
mainOutputsLayout = new QFormLayout;
mainOutputsLayout->setContentsMargins(9, 2, 9, 9);
mainOutputsLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
mainOutputsLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignTrailing | Qt::AlignVCenter);
auto streaming_title_layout = new QHBoxLayout;
auto streaming_title = new QLabel(QString::fromUtf8(obs_module_text("MainCanvas")));
streaming_title->setStyleSheet(QString::fromUtf8("font-weight: bold;"));
streaming_title_layout->addWidget(streaming_title, 0, Qt::AlignLeft);
//auto guide_link = new QLabel(QString::fromUtf8("<a href=\"https://l.aitum.tv/vh-streaming-settings\">") + QString::fromUtf8(obs_module_text("ViewGuide")) + QString::fromUtf8("</a>"));
//guide_link->setOpenExternalLinks(true);
auto addButton = new QPushButton(QIcon(":/res/images/plus.svg"), QString::fromUtf8(obs_module_text("AddOutput")));
addButton->setProperty("themeID", QVariant(QString::fromUtf8("addIconSmall")));
connect(addButton, &QPushButton::clicked, [this] {
if (!settings)
return;
auto outputs = obs_data_get_array(settings, "outputs");
if (!outputs) {
outputs = obs_data_array_create();
obs_data_set_array(settings, "outputs", outputs);
}
auto s = obs_data_create();
obs_data_set_string(s, "name", obs_module_text("Unnamed"));
obs_data_array_push_back(outputs, s);
obs_data_array_release(outputs);
AddServer(mainOutputsLayout, s);
obs_data_release(s);
});
//streaming_title_layout->addWidget(guide_link, 0, Qt::AlignRight);
streaming_title_layout->addWidget(addButton, 0, Qt::AlignRight);
mainOutputsLayout->addRow(streaming_title_layout);
auto serverGroup = new QGroupBox;
serverGroup->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
serverGroup->setStyleSheet(QString("QGroupBox{background-color: %1; padding-top: 4px;}")
.arg(palette().color(QPalette::ColorRole::Mid).name(QColor::HexRgb)));
auto serverLayout = new QFormLayout;
serverLayout->setContentsMargins(9, 2, 9, 9);
serverLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
serverLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignTrailing | Qt::AlignVCenter);
auto main_title = new QLabel(QString::fromUtf8(obs_module_text("MainOutput")));
main_title->setStyleSheet(QString::fromUtf8("font-weight: bold;"));
serverLayout->addRow(main_title);
serverGroup->setLayout(serverLayout);
mainOutputsLayout->addRow(serverGroup);
mainOutputsPage->setLayout(mainOutputsLayout);
const auto version =
new QLabel(QString::fromUtf8(obs_module_text("Version")) + " " + QString::fromUtf8(PROJECT_VERSION) + " " +
QString::fromUtf8(obs_module_text("MadeBy")) + " <a href=\"https://aitum.tv\">Aitum</a>");
version->setOpenExternalLinks(true);
version->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
QPushButton *okButton = new QPushButton(QString::fromUtf8(obs_frontend_get_locale_string("OK")));
connect(okButton, &QPushButton::clicked, [this] { accept(); });
QPushButton *cancelButton = new QPushButton(QString::fromUtf8(obs_frontend_get_locale_string("Cancel")));
connect(cancelButton, &QPushButton::clicked, [this] { reject(); });
QHBoxLayout *bottomLayout = new QHBoxLayout;
bottomLayout->addWidget(version, 1, Qt::AlignLeft);
//bottomLayout->addWidget(newVersion, 1, Qt::AlignLeft);
bottomLayout->addWidget(okButton, 0, Qt::AlignRight);
bottomLayout->addWidget(cancelButton, 0, Qt::AlignRight);
QHBoxLayout *contentLayout = new QHBoxLayout;
contentLayout->addWidget(listWidget);
contentLayout->addWidget(settingsPages, 1);
listWidget->connect(listWidget, &QListWidget::currentRowChanged, settingsPages, &QStackedWidget::setCurrentIndex);
listWidget->setCurrentRow(1);
QVBoxLayout *vlayout = new QVBoxLayout;
vlayout->setContentsMargins(11, 11, 11, 11);
vlayout->addLayout(contentLayout);
vlayout->addLayout(bottomLayout);
setLayout(vlayout);
}
OBSBasicSettings::~OBSBasicSettings() {}
QIcon OBSBasicSettings::GetGeneralIcon() const
{
return listWidget->item(0)->icon();
}
QIcon OBSBasicSettings::GetStreamIcon() const
{
return listWidget->item(1)->icon();
}
QIcon OBSBasicSettings::GetOutputIcon() const
{
return QIcon();
}
QIcon OBSBasicSettings::GetAudioIcon() const
{
return QIcon();
}
QIcon OBSBasicSettings::GetVideoIcon() const
{
return QIcon();
}
QIcon OBSBasicSettings::GetHotkeysIcon() const
{
return QIcon();
}
QIcon OBSBasicSettings::GetAccessibilityIcon() const
{
return QIcon();
}
QIcon OBSBasicSettings::GetAdvancedIcon() const
{
return QIcon();
}
void OBSBasicSettings::SetGeneralIcon(const QIcon &icon)
{
listWidget->item(0)->setIcon(icon);
}
void OBSBasicSettings::SetStreamIcon(const QIcon &icon)
{
listWidget->item(1)->setIcon(icon);
//listWidget->item(2)->setIcon(icon);
}
void OBSBasicSettings::SetOutputIcon(const QIcon &icon)
{
UNUSED_PARAMETER(icon);
//listWidget->item(2)->setIcon(icon);
}
void OBSBasicSettings::SetAudioIcon(const QIcon &icon)
{
UNUSED_PARAMETER(icon);
}
void OBSBasicSettings::SetVideoIcon(const QIcon &icon)
{
UNUSED_PARAMETER(icon);
}
void OBSBasicSettings::SetHotkeysIcon(const QIcon &icon)
{
UNUSED_PARAMETER(icon);
}
void OBSBasicSettings::SetAccessibilityIcon(const QIcon &icon)
{
UNUSED_PARAMETER(icon);
}
void OBSBasicSettings::SetAdvancedIcon(const QIcon &icon)
{
UNUSED_PARAMETER(icon);
}
void OBSBasicSettings::AddServer(QFormLayout *outputsLayout, obs_data_t *settings)
{
auto serverGroup = new QGroupBox;
serverGroup->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
serverGroup->setProperty("altColor", QVariant(true));
serverGroup->setStyleSheet(QString("QGroupBox[altColor=\"true\"]{background-color: %1; padding-top: 4px;}")
.arg(palette().color(QPalette::ColorRole::Mid).name(QColor::HexRgb)));
auto serverLayout = new QFormLayout;
serverLayout->setContentsMargins(9, 2, 9, 9);
serverLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
serverLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignTrailing | Qt::AlignVCenter);
auto server_title_layout = new QHBoxLayout;
auto streaming_title = new QLabel(QString::fromUtf8(obs_data_get_string(settings, "name")));
streaming_title->setStyleSheet(QString::fromUtf8("font-weight: bold;"));
auto title_stack = new QStackedWidget;
title_stack->addWidget(streaming_title);
auto title_edit = new QLineEdit;
connect(title_edit, &QLineEdit::textChanged,
[title_edit, settings] { obs_data_set_string(settings, "name", title_edit->text().toUtf8().constData()); });
title_stack->addWidget(title_edit);
title_stack->setCurrentWidget(streaming_title);
server_title_layout->addWidget(title_stack, 1, Qt::AlignLeft);
auto configButton = new QPushButton;
configButton->setMinimumHeight(30);
auto renameButton = new QPushButton(QString::fromUtf8(obs_frontend_get_locale_string("Rename")));
renameButton->setCheckable(true);
connect(renameButton, &QPushButton::clicked, [renameButton, title_stack, title_edit, streaming_title, settings] {
if (title_stack->currentWidget() == title_edit) {
streaming_title->setText(title_edit->text());
title_stack->setCurrentWidget(streaming_title);
} else {
title_edit->setText(streaming_title->text());
title_stack->setCurrentWidget(title_edit);
}
});
//renameButton->setProperty("themeID", "configIconSmall");
server_title_layout->addWidget(renameButton, 0, Qt::AlignRight);
const bool advanced = obs_data_get_bool(settings, "advanced");
auto advancedGroup = new QGroupBox(QString::fromUtf8(obs_frontend_get_locale_string("Advanced")));
advancedGroup->setVisible(advanced);
auto advancedGroupLayout = new QFormLayout;
advancedGroup->setLayout(advancedGroupLayout);
auto videoEncoder = new QComboBox;
videoEncoder->addItem(QString::fromUtf8(obs_module_text("MainEncoder")), QVariant(QString::fromUtf8("")));
videoEncoder->setCurrentIndex(0);
advancedGroupLayout->addRow(QString::fromUtf8(obs_module_text("VideoEncoder")), videoEncoder);
auto videoEncoderIndex = new QComboBox;
for (int i = 0; i < MAX_OUTPUT_VIDEO_ENCODERS; i++) {
videoEncoderIndex->addItem(QString::number(i + 1));
}
videoEncoderIndex->setCurrentIndex(obs_data_get_int(settings, "video_encoder_index"));
connect(videoEncoderIndex, &QComboBox::currentIndexChanged,
[videoEncoderIndex, settings] {
if (videoEncoderIndex->currentIndex() >= 0)
obs_data_set_int(settings, "video_encoder_index", videoEncoderIndex->currentIndex());
});
advancedGroupLayout->addRow(QString::fromUtf8(obs_module_text("VideoEncoderIndex")), videoEncoderIndex);
auto videoEncoderGroup = new QGroupBox(QString::fromUtf8(obs_module_text("VideoEncoder")));
videoEncoderGroup->setProperty("altColor", QVariant(true));
auto videoEncoderGroupLayout = new QFormLayout();
videoEncoderGroup->setLayout(videoEncoderGroupLayout);
advancedGroupLayout->addRow(videoEncoderGroup);
connect(videoEncoder, &QComboBox::currentIndexChanged,
[this, serverGroup, advancedGroupLayout, videoEncoder, videoEncoderIndex, videoEncoderGroup,
videoEncoderGroupLayout, settings] {
auto encoder_string = videoEncoder->currentData().toString().toUtf8();
auto encoder = encoder_string.constData();
obs_data_set_string(settings, "video_encoder", encoder);
if (!encoder || encoder[0] == '\0') {
advancedGroupLayout->setRowVisible(videoEncoderIndex, true);
videoEncoderGroup->setVisible(false);
} else {
advancedGroupLayout->setRowVisible(videoEncoderIndex, false);
videoEncoderGroup->setVisible(true);
auto t = video_encoder_properties.find(serverGroup);
if (t != video_encoder_properties.end()) {
obs_properties_destroy(t->second);
video_encoder_properties.erase(t);
}
for (int i = videoEncoderGroupLayout->rowCount() - 1; i >=0; i--) {
videoEncoderGroupLayout->removeRow(i);
}
//auto stream_encoder_settings = obs_encoder_defaults(encoder);
auto ves = obs_data_get_obj(settings, "video_encoder_settings");
if (!ves) {
ves = obs_encoder_defaults(encoder);
obs_data_set_obj(settings, "video_encoder_settings", ves);
}
auto stream_encoder_properties = obs_get_encoder_properties(encoder);
video_encoder_properties[serverGroup] = stream_encoder_properties;
obs_property_t *property = obs_properties_first(stream_encoder_properties);
while (property) {
AddProperty(stream_encoder_properties, property, ves, videoEncoderGroupLayout,
&encoder_property_widgets);
obs_property_next(&property);
}
obs_data_release(ves);
//obs_properties_destroy(stream_encoder_properties);
}
});
const char *current_type = obs_data_get_string(settings, "video_encoder");
const char *type;
size_t idx = 0;
while (obs_enum_encoder_types(idx++, &type)) {
if (obs_get_encoder_type(type) != OBS_ENCODER_VIDEO)
continue;
uint32_t caps = obs_get_encoder_caps(type);
if ((caps & (OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL)) != 0)
continue;
const char *codec = obs_get_encoder_codec(type);
if (astrcmpi(codec, "h264") != 0 && astrcmpi(codec, "hevc") != 0 && astrcmpi(codec, "av1") != 0)
continue;
videoEncoder->addItem(QString::fromUtf8(obs_encoder_get_display_name(type)), QVariant(QString::fromUtf8(type)));
if (strcmp(type, current_type) == 0)
videoEncoder->setCurrentIndex(videoEncoder->count() - 1);
}
videoEncoderGroup->setVisible(advanced && videoEncoder->currentIndex() > 0);
auto advancedButton = new QPushButton(QString::fromUtf8(obs_frontend_get_locale_string("Advanced")));
advancedButton->setProperty("themeID", "configIconSmall");
advancedButton->setCheckable(true);
advancedButton->setChecked(advanced);
connect(advancedButton, &QPushButton::clicked, [advancedButton, advancedGroup, settings] {
const bool advanced = advancedButton->isChecked();
advancedGroup->setVisible(advanced);
obs_data_set_bool(settings, "advanced", advanced);
});
server_title_layout->addWidget(advancedButton, 0, Qt::AlignRight);
auto removeButton =
new QPushButton(QIcon(":/res/images/minus.svg"), QString::fromUtf8(obs_frontend_get_locale_string("Remove")));
removeButton->setProperty("themeID", QVariant(QString::fromUtf8("removeIconSmall")));
connect(removeButton, &QPushButton::clicked, [this, outputsLayout, serverGroup, settings] {
if (serverGroup->layout()) {
QLayoutItem *item;
while ((item = serverGroup->layout()->takeAt(0)) != NULL) {
delete item->widget();
delete item;
}
delete serverGroup->layout();
}
outputsLayout->removeWidget(serverGroup);
delete serverGroup;
auto outputs = obs_data_get_array(this->settings, "outputs");
auto count = obs_data_array_count(outputs);
for (size_t i = 0; i < count; i++) {
auto item = obs_data_array_item(outputs, i);
if (item == settings) {
obs_data_array_erase(outputs, i);
obs_data_release(item);
break;
}
obs_data_release(item);
}
obs_data_array_release(outputs);
});
server_title_layout->addWidget(removeButton, 0, Qt::AlignRight);
serverLayout->addRow(server_title_layout);
serverLayout->addRow(advancedGroup);
auto server = new QComboBox;
server->setEditable(true);
server->addItem("rtmps://a.rtmps.youtube.com:443/live2");
server->addItem("rtmps://b.rtmps.youtube.com:443/live2?backup=1");
server->addItem("rtmp://a.rtmp.youtube.com/live2");
server->addItem("rtmp://b.rtmp.youtube.com/live2?backup=1");
server->setCurrentText(QString::fromUtf8(obs_data_get_string(settings, "server")));
connect(server, &QComboBox::currentTextChanged,
[server, settings] { obs_data_set_string(settings, "server", server->currentText().toUtf8().constData()); });
serverLayout->addRow(QString::fromUtf8(obs_module_text("Server")), server);
QLayout *subLayout = new QHBoxLayout();
auto key = new QLineEdit;
key->setEchoMode(QLineEdit::Password);
key->setText(QString::fromUtf8(obs_data_get_string(settings, "key")));
connect(key, &QLineEdit::textChanged,
[key, settings] { obs_data_set_string(settings, "key", key->text().toUtf8().constData()); });
QPushButton *show = new QPushButton();
show->setText(QString::fromUtf8(obs_frontend_get_locale_string("Show")));
show->setCheckable(true);
show->connect(show, &QAbstractButton::toggled, [=](bool hide) {
show->setText(
QString::fromUtf8(hide ? obs_frontend_get_locale_string("Hide") : obs_frontend_get_locale_string("Show")));
key->setEchoMode(hide ? QLineEdit::Normal : QLineEdit::Password);
});
subLayout->addWidget(key);
subLayout->addWidget(show);
serverLayout->addRow(QString::fromUtf8(obs_module_text("Key")), subLayout);
serverGroup->setLayout(serverLayout);
outputsLayout->addRow(serverGroup);
}
void OBSBasicSettings::LoadSettings(obs_data_t *settings)
{
while (mainOutputsLayout->rowCount() > 2) {
auto i = mainOutputsLayout->takeRow(2).fieldItem;
if (i->widget()) {
if (i->widget()->layout()) {
QLayoutItem *item;
while ((item = i->widget()->layout()->takeAt(0)) != NULL) {
delete item->widget();
delete item;
}
delete i->widget()->layout();
}
delete i->widget();
}
mainOutputsLayout->removeRow(2);
}
this->settings = settings;
auto outputs = obs_data_get_array(settings, "outputs");
obs_data_array_enum(
outputs,
[](obs_data_t *data, void *param) {
auto d = (OBSBasicSettings *)param;
d->AddServer(d->mainOutputsLayout, data);
},
this);
obs_data_array_release(outputs);
}
void OBSBasicSettings::AddProperty(obs_properties_t *properties, obs_property_t *property, obs_data_t *settings,
QFormLayout *layout, std::map<obs_property_t *, QWidget *> *widgets)
{
obs_property_type type = obs_property_get_type(property);
if (type == OBS_PROPERTY_BOOL) {
auto widget = new QCheckBox(QString::fromUtf8(obs_property_description(property)));
widget->setChecked(obs_data_get_bool(settings, obs_property_name(property)));
layout->addWidget(widget);
if (!obs_property_visible(property)) {
widget->setVisible(false);
int row = 0;
layout->getWidgetPosition(widget, &row, nullptr);
auto item = layout->itemAt(row, QFormLayout::LabelRole);
if (item) {
auto w = item->widget();
if (w)
w->setVisible(false);
}
}
widgets->emplace(property, widget);
connect(widget, &QCheckBox::stateChanged, [this, properties, property, settings, widget, widgets, layout] {
obs_data_set_bool(settings, obs_property_name(property), widget->isChecked());
if (obs_property_modified(property, settings)) {
RefreshProperties(properties, widgets, layout);
}
});
} else if (type == OBS_PROPERTY_INT) {
auto widget = new QSpinBox();
widget->setEnabled(obs_property_enabled(property));
widget->setMinimum(obs_property_int_min(property));
widget->setMaximum(obs_property_int_max(property));
widget->setSingleStep(obs_property_int_step(property));
widget->setValue((int)obs_data_get_int(settings, obs_property_name(property)));
widget->setToolTip(QString::fromUtf8(obs_property_long_description(property)));
widget->setSuffix(QString::fromUtf8(obs_property_int_suffix(property)));
auto label = new QLabel(QString::fromUtf8(obs_property_description(property)));
layout->addRow(label, widget);
if (!obs_property_visible(property)) {
widget->setVisible(false);
label->setVisible(false);
}
widgets->emplace(property, widget);
connect(widget, &QSpinBox::valueChanged, [this, properties, property, settings, widget, widgets, layout] {
obs_data_set_int(settings, obs_property_name(property), widget->value());
if (obs_property_modified(property, settings)) {
RefreshProperties(properties, widgets, layout);
}
});
} else if (type == OBS_PROPERTY_FLOAT) {
auto widget = new QDoubleSpinBox();
widget->setEnabled(obs_property_enabled(property));
widget->setMinimum(obs_property_float_min(property));
widget->setMaximum(obs_property_float_max(property));
widget->setSingleStep(obs_property_float_step(property));
widget->setValue(obs_data_get_double(settings, obs_property_name(property)));
widget->setToolTip(QString::fromUtf8(obs_property_long_description(property)));
widget->setSuffix(QString::fromUtf8(obs_property_float_suffix(property)));
auto label = new QLabel(QString::fromUtf8(obs_property_description(property)));
layout->addRow(label, widget);
if (!obs_property_visible(property)) {
widget->setVisible(false);
label->setVisible(false);
}
widgets->emplace(property, widget);
connect(widget, &QDoubleSpinBox::valueChanged, [this, properties, property, settings, widget, widgets, layout] {
obs_data_set_double(settings, obs_property_name(property), widget->value());
if (obs_property_modified(property, settings)) {
RefreshProperties(properties, widgets, layout);
}
});
} else if (type == OBS_PROPERTY_TEXT) {
obs_text_type text_type = obs_property_text_type(property);
if (text_type == OBS_TEXT_MULTILINE) {
auto widget = new QPlainTextEdit;
widget->document()->setDefaultStyleSheet("font { white-space: pre; }");
widget->setTabStopDistance(40);
widget->setPlainText(QString::fromUtf8(obs_data_get_string(settings, obs_property_name(property))));
auto label = new QLabel(QString::fromUtf8(obs_property_description(property)));
layout->addRow(label, widget);
if (!obs_property_visible(property)) {
widget->setVisible(false);
label->setVisible(false);
}
widgets->emplace(property, widget);
connect(widget, &QPlainTextEdit::textChanged,
[this, properties, property, settings, widget, widgets, layout] {
obs_data_set_string(settings, obs_property_name(property), widget->toPlainText().toUtf8());
if (obs_property_modified(property, settings)) {
RefreshProperties(properties, widgets, layout);
}
});
} else {
auto widget = new QLineEdit();
widget->setText(QString::fromUtf8(obs_data_get_string(settings, obs_property_name(property))));
if (text_type == OBS_TEXT_PASSWORD)
widget->setEchoMode(QLineEdit::Password);
auto label = new QLabel(QString::fromUtf8(obs_property_description(property)));
layout->addRow(label, widget);
if (!obs_property_visible(property)) {
widget->setVisible(false);
label->setVisible(false);
}
widgets->emplace(property, widget);
if (text_type != OBS_TEXT_INFO) {
connect(widget, &QLineEdit::textChanged,
[this, properties, property, settings, widget, widgets, layout] {
obs_data_set_string(settings, obs_property_name(property), widget->text().toUtf8());
if (obs_property_modified(property, settings)) {
RefreshProperties(properties, widgets, layout);
}
});
}
}
} else if (type == OBS_PROPERTY_LIST) {
auto widget = new QComboBox();
widget->setMaxVisibleItems(40);
widget->setToolTip(QString::fromUtf8(obs_property_long_description(property)));
auto list_type = obs_property_list_type(property);
obs_combo_format format = obs_property_list_format(property);
size_t count = obs_property_list_item_count(property);
for (size_t i = 0; i < count; i++) {
QVariant var;
if (format == OBS_COMBO_FORMAT_INT) {
long long val = obs_property_list_item_int(property, i);
var = QVariant::fromValue<long long>(val);
} else if (format == OBS_COMBO_FORMAT_FLOAT) {
double val = obs_property_list_item_float(property, i);
var = QVariant::fromValue<double>(val);
} else if (format == OBS_COMBO_FORMAT_STRING) {
var = QByteArray(obs_property_list_item_string(property, i));
}
widget->addItem(QString::fromUtf8(obs_property_list_item_name(property, i)), var);
}
if (list_type == OBS_COMBO_TYPE_EDITABLE)
widget->setEditable(true);
auto name = obs_property_name(property);
QVariant value;
switch (format) {
case OBS_COMBO_FORMAT_INT:
value = QVariant::fromValue(obs_data_get_int(settings, name));
break;
case OBS_COMBO_FORMAT_FLOAT:
value = QVariant::fromValue(obs_data_get_double(settings, name));
break;
case OBS_COMBO_FORMAT_STRING:
value = QByteArray(obs_data_get_string(settings, name));
break;
default:;
}
if (format == OBS_COMBO_FORMAT_STRING && list_type == OBS_COMBO_TYPE_EDITABLE) {
widget->lineEdit()->setText(value.toString());
} else {
auto idx = widget->findData(value);
if (idx != -1)
widget->setCurrentIndex(idx);
}
if (obs_data_has_autoselect_value(settings, name)) {
switch (format) {
case OBS_COMBO_FORMAT_INT:
value = QVariant::fromValue(obs_data_get_autoselect_int(settings, name));
break;
case OBS_COMBO_FORMAT_FLOAT:
value = QVariant::fromValue(obs_data_get_autoselect_double(settings, name));
break;
case OBS_COMBO_FORMAT_STRING:
value = QByteArray(obs_data_get_autoselect_string(settings, name));
break;
default:;
}
int id = widget->findData(value);
auto idx = widget->currentIndex();
if (id != -1 && id != idx) {
QString actual = widget->itemText(id);
QString selected = widget->itemText(widget->currentIndex());
QString combined = QString::fromUtf8(
obs_frontend_get_locale_string("Basic.PropertiesWindow.AutoSelectFormat"));
widget->setItemText(idx, combined.arg(selected).arg(actual));
}
}
auto label = new QLabel(QString::fromUtf8(obs_property_description(property)));
layout->addRow(label, widget);
if (!obs_property_visible(property)) {
widget->setVisible(false);
label->setVisible(false);
}
widgets->emplace(property, widget);
switch (format) {
case OBS_COMBO_FORMAT_INT:
connect(widget, &QComboBox::currentIndexChanged,
[this, properties, property, settings, widget, widgets, layout] {
obs_data_set_int(settings, obs_property_name(property), widget->currentData().toInt());
if (obs_property_modified(property, settings)) {
RefreshProperties(properties, widgets, layout);
}
});
break;
case OBS_COMBO_FORMAT_FLOAT:
connect(widget, &QComboBox::currentIndexChanged,
[this, properties, property, settings, widget, widgets, layout] {
obs_data_set_double(settings, obs_property_name(property),
widget->currentData().toDouble());
if (obs_property_modified(property, settings)) {
RefreshProperties(properties, widgets, layout);
}
});
break;
case OBS_COMBO_FORMAT_STRING:
if (list_type == OBS_COMBO_TYPE_EDITABLE) {
connect(widget, &QComboBox::currentTextChanged,
[this, properties, property, settings, widget, widgets, layout] {
obs_data_set_string(settings, obs_property_name(property),
widget->currentText().toUtf8().constData());
if (obs_property_modified(property, settings)) {
RefreshProperties(properties, widgets, layout);
}
});
} else {
connect(widget, &QComboBox::currentIndexChanged,
[this, properties, property, settings, widget, widgets, layout] {
obs_data_set_string(settings, obs_property_name(property),
widget->currentData().toString().toUtf8().constData());
if (obs_property_modified(property, settings)) {
RefreshProperties(properties, widgets, layout);
}
});
}
break;
default:;
}
} else {
// OBS_PROPERTY_PATH
// OBS_PROPERTY_COLOR
// OBS_PROPERTY_BUTTON
// OBS_PROPERTY_FONT
// OBS_PROPERTY_EDITABLE_LIST
// OBS_PROPERTY_FRAME_RATE
// OBS_PROPERTY_GROUP
// OBS_PROPERTY_COLOR_ALPHA
}
obs_property_modified(property, settings);
}
void OBSBasicSettings::RefreshProperties(obs_properties_t *properties, std::map<obs_property_t *, QWidget *> *widgets,
QFormLayout *layout)
{
obs_property_t *property = obs_properties_first(properties);
while (property) {
auto widget = widgets->at(property);
auto visible = obs_property_visible(property);
if (widget->isVisible() != visible) {
widget->setVisible(visible);
int row = 0;
layout->getWidgetPosition(widget, &row, nullptr);
auto item = layout->itemAt(row, QFormLayout::LabelRole);
if (item) {
widget = item->widget();
if (widget)
widget->setVisible(visible);
}
}
obs_property_next(&property);
}
}

71
config-dialog.hpp Normal file
View File

@ -0,0 +1,71 @@
#pragma once
#include "multistream.hpp"
#include <QCheckBox>
#include <QComboBox>
#include <QDialog>
#include <QMainWindow>
#include <QTextEdit>
#include <obs-frontend-api.h>
#include <QGroupBox>
#include <QListWidget>
#include <QSpinBox>
#include <QFormLayout>
#include <QRadioButton>
class MultistreamDock;
class OBSBasicSettings : public QDialog {
Q_OBJECT
Q_PROPERTY(QIcon generalIcon READ GetGeneralIcon WRITE SetGeneralIcon DESIGNABLE true)
Q_PROPERTY(QIcon streamIcon READ GetStreamIcon WRITE SetStreamIcon DESIGNABLE true)
Q_PROPERTY(QIcon outputIcon READ GetOutputIcon WRITE SetOutputIcon DESIGNABLE true)
Q_PROPERTY(QIcon audioIcon READ GetAudioIcon WRITE SetAudioIcon DESIGNABLE true)
Q_PROPERTY(QIcon videoIcon READ GetVideoIcon WRITE SetVideoIcon DESIGNABLE true)
Q_PROPERTY(QIcon hotkeysIcon READ GetHotkeysIcon WRITE SetHotkeysIcon DESIGNABLE true)
Q_PROPERTY(QIcon accessibilityIcon READ GetAccessibilityIcon WRITE SetAccessibilityIcon DESIGNABLE true)
Q_PROPERTY(QIcon advancedIcon READ GetAdvancedIcon WRITE SetAdvancedIcon DESIGNABLE true)
private:
QListWidget *listWidget;
QIcon GetGeneralIcon() const;
QIcon GetStreamIcon() const;
QIcon GetOutputIcon() const;
QIcon GetAudioIcon() const;
QIcon GetVideoIcon() const;
QIcon GetHotkeysIcon() const;
QIcon GetAccessibilityIcon() const;
QIcon GetAdvancedIcon() const;
void AddServer(QFormLayout *outputsLayout, obs_data_t *settings);
void AddProperty(obs_properties_t *properties, obs_property_t *property, obs_data_t *settings, QFormLayout *layout,
std::map<obs_property_t *, QWidget *> *widgets);
void RefreshProperties(obs_properties_t *properties, std::map<obs_property_t *, QWidget *> *widgets, QFormLayout *layout);
obs_data_t *settings;
std::map<obs_property_t *, QWidget *> encoder_property_widgets;
std::map<QWidget *, obs_properties_t *> video_encoder_properties;
QFormLayout *mainOutputsLayout;
QFormLayout *verticalOutputsLayout;
private slots:
void SetGeneralIcon(const QIcon &icon);
void SetStreamIcon(const QIcon &icon);
void SetOutputIcon(const QIcon &icon);
void SetAudioIcon(const QIcon &icon);
void SetVideoIcon(const QIcon &icon);
void SetHotkeysIcon(const QIcon &icon);
void SetAccessibilityIcon(const QIcon &icon);
void SetAdvancedIcon(const QIcon &icon);
public:
OBSBasicSettings(QMainWindow *parent = nullptr);
~OBSBasicSettings();
void LoadSettings(obs_data_t* settings);
public slots:
};

16
data/locale/en-US.ini Normal file
View File

@ -0,0 +1,16 @@
Aitum="Aitum"
AitumMultistream="Aitum Multistream"
AitumMultistreamSettings="Aitum Multistream Settings"
MainCanvas="Main Canvas"
BuiltinStream="Built-in stream"
Stream="Stream"
VerticalCanvas="Vertical Canvas"
General="General"
MainCanvas="Main Canvas"
MainOutput="Main Output"
SetupTroubleshooter="Setup Troubleshooter"
Help="Help"
AddOutput="AddOutput"
Unnamed="Unnamed"
Version="Version"
MadeBy="made with ♡ by"

145
file-updater.c Normal file
View File

@ -0,0 +1,145 @@
#include <util/curl/curl-helper.h>
#include <util/threading.h>
#include <util/platform.h>
#include <util/darray.h>
#include <util/dstr.h>
#include <obs-data.h>
#include "file-updater.h"
#define warn(msg, ...) blog(LOG_WARNING, "%s" msg, info->log_prefix, ##__VA_ARGS__)
#define info(msg, ...) blog(LOG_WARNING, "%s" msg, info->log_prefix, ##__VA_ARGS__)
struct update_info {
char error[CURL_ERROR_SIZE];
struct curl_slist *header;
DARRAY(uint8_t) file_data;
char *user_agent;
CURL *curl;
char *url;
confirm_file_callback_t callback;
void *param;
pthread_t thread;
bool thread_created;
char *log_prefix;
};
void update_info_destroy(struct update_info *info)
{
if (!info)
return;
if (info->thread_created)
pthread_join(info->thread, NULL);
da_free(info->file_data);
bfree(info->log_prefix);
bfree(info->user_agent);
bfree(info->url);
if (info->header)
curl_slist_free_all(info->header);
if (info->curl)
curl_easy_cleanup(info->curl);
bfree(info);
}
static size_t http_write(void *ptr, size_t size, size_t nmemb, void *uinfo)
{
size_t total = size * nmemb;
struct update_info *info = (struct update_info *)uinfo;
if (total)
da_push_back_array(info->file_data, ptr, total);
return total;
}
static bool do_http_request(struct update_info *info, const char *url, long *response_code)
{
CURLcode code;
uint8_t null_terminator = 0;
da_resize(info->file_data, 0);
curl_easy_setopt(info->curl, CURLOPT_URL, url);
curl_easy_setopt(info->curl, CURLOPT_HTTPHEADER, info->header);
curl_easy_setopt(info->curl, CURLOPT_ERRORBUFFER, info->error);
curl_easy_setopt(info->curl, CURLOPT_WRITEFUNCTION, http_write);
curl_easy_setopt(info->curl, CURLOPT_WRITEDATA, info);
curl_easy_setopt(info->curl, CURLOPT_FAILONERROR, true);
curl_easy_setopt(info->curl, CURLOPT_NOSIGNAL, 1);
curl_easy_setopt(info->curl, CURLOPT_ACCEPT_ENCODING, "");
curl_obs_set_revoke_setting(info->curl);
code = curl_easy_perform(info->curl);
if (code != CURLE_OK) {
warn("Remote update of URL \"%s\" failed: %s", url, info->error);
return false;
}
if (curl_easy_getinfo(info->curl, CURLINFO_RESPONSE_CODE, response_code) != CURLE_OK)
return false;
if (*response_code >= 400) {
warn("Remote update of URL \"%s\" failed: HTTP/%ld", url, *response_code);
return false;
}
da_push_back(info->file_data, &null_terminator);
return true;
}
struct file_update_data {
const char *name;
int version;
bool newer;
bool found;
};
static void *single_file_thread(void *data)
{
struct update_info *info = data;
struct file_download_data download_data;
long response_code;
info->curl = curl_easy_init();
if (!info->curl) {
warn("Could not initialize Curl");
return NULL;
}
if (!do_http_request(info, info->url, &response_code))
return NULL;
if (!info->file_data.array || !info->file_data.array[0])
return NULL;
download_data.name = info->url;
download_data.version = 0;
download_data.buffer.da = info->file_data.da;
info->callback(info->param, &download_data);
info->file_data.da = download_data.buffer.da;
return NULL;
}
update_info_t *update_info_create_single(const char *log_prefix, const char *user_agent, const char *file_url,
confirm_file_callback_t confirm_callback, void *param)
{
struct update_info *info;
if (!log_prefix)
log_prefix = "";
info = bzalloc(sizeof(*info));
info->log_prefix = bstrdup(log_prefix);
info->user_agent = bstrdup(user_agent);
info->url = bstrdup(file_url);
info->callback = confirm_callback;
info->param = param;
if (pthread_create(&info->thread, NULL, single_file_thread, info) == 0)
info->thread_created = true;
return info;
}

19
file-updater.h Normal file
View File

@ -0,0 +1,19 @@
#pragma once
#include <util/darray.h>
struct update_info;
typedef struct update_info update_info_t;
struct file_download_data {
const char *name;
int version;
DARRAY(uint8_t) buffer;
};
typedef bool (*confirm_file_callback_t)(void *param, struct file_download_data *file);
update_info_t *update_info_create_single(const char *log_prefix, const char *user_agent, const char *file_url,
confirm_file_callback_t confirm_callback, void *param);
void update_info_destroy(update_info_t *info);

161
installer.iss.in Normal file
View File

@ -0,0 +1,161 @@
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "@PROJECT_FULL_NAME@"
#define MyAppVersion "@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@"
#define MyAppPublisher "Aitum"
[Setup]
; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
; app Information
AppId={{7E8814F2-03EE-4E2E-8062-A9A184EC0618}}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppMutex={#MyAppName}
VersionInfoVersion={#MyAppVersion}
VersionInfoCompany={#MyAppPublisher}
VersionInfoDescription={#MyAppName} Setup
; Compression
Compression=lzma2/ultra64
SolidCompression=yes
LZMAAlgorithm=1
; Other Information
DefaultDirName={code:GetDirName}
DefaultGroupName={#MyAppName}
AllowNoIcons=yes
OutputDir="package"
OutputBaseFilename=@PROJECT_NAME@-installer
DirExistsWarning=no
DisableDirPage=no
; Wizard Information
WizardStyle=modern
WizardResizable=yes
SetupIconFile="media/icon.ico"
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
Source: "package/*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "msvc-redist-helper.exe"; DestDir: "{app}"; DestName: "msvc-redist-helper.exe"; Flags: ignoreversion dontcopy noencryption
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
[Code]
function GetDirName(Value: string): string;
var
InstallPath: string;
begin
// initialize default path, which will be returned when the following registry
// key queries fail due to missing keys or for some different reason
Result := ExpandConstant('{pf}\obs-studio');
// query the first registry value; if this succeeds, return the obtained value
if RegQueryStringValue(HKLM32, 'SOFTWARE\OBS Studio', '', InstallPath) then
Result := InstallPath
end;
/////////////////////////////////////////////////////////////////////
function GetUninstallString(): String;
var
sUnInstPath: String;
sUnInstallString: String;
begin
sUnInstPath := ExpandConstant('Software\Microsoft\Windows\CurrentVersion\Uninstall\{#emit SetupSetting("AppId")}_is1');
sUnInstallString := '';
if not RegQueryStringValue(HKLM, sUnInstPath, 'UninstallString', sUnInstallString) then
RegQueryStringValue(HKCU, sUnInstPath, 'UninstallString', sUnInstallString);
Result := sUnInstallString;
end;
/////////////////////////////////////////////////////////////////////
function IsUpgrade(): Boolean;
begin
Result := (GetUninstallString() <> '');
end;
/////////////////////////////////////////////////////////////////////
function UnInstallOldVersion(): Integer;
var
sUnInstallString: String;
iResultCode: Integer;
begin
// Return Values:
// 1 - uninstall string is empty
// 2 - error executing the UnInstallString
// 3 - successfully executed the UnInstallString
// default return value
Result := 0;
// get the uninstall string of the old app
sUnInstallString := GetUninstallString();
if sUnInstallString <> '' then begin
sUnInstallString := RemoveQuotes(sUnInstallString);
if Exec(sUnInstallString, '/VERYSILENT /NORESTART /SUPPRESSMSGBOXES','', SW_HIDE, ewWaitUntilTerminated, iResultCode) then
Result := 3
else
Result := 2;
end else
Result := 1;
end;
/////////////////////////////////////////////////////////////////////
procedure CurStepChanged(CurStep: TSetupStep);
var
ResultCode: Integer;
begin
if (CurStep=ssInstall) then
begin
if (IsUpgrade()) then
begin
UnInstallOldVersion();
end;
end;
if (CurStep=ssPostInstall) then
begin
ExtractTemporaryFile('msvc-redist-helper.exe');
Exec(ExpandConstant('{tmp}\msvc-redist-helper.exe'), '2019', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
end;
/////////////////////////////////////////////////////////////////////
function NextButtonClick(PageId: Integer): Boolean;
var
ObsFileName: string;
ObsMS, ObsLS: Cardinal;
ObsMajorVersion, ObsMinorVersion: Cardinal;
begin
Result := True;
if not (PageId = wpSelectDir) then begin
exit;
end;
ObsFileName := ExpandConstant('{app}\bin\64bit\obs64.exe');
if not FileExists(ObsFileName) then begin
MsgBox('OBS Studio (bin\64bit\obs64.exe) does not seem to be installed in that folder. Please select the correct folder.', mbError, MB_OK);
Result := False;
exit;
end;
Result := GetVersionNumbers(ObsFileName, ObsMS, ObsLS);
if not Result then begin
MsgBox('Failed to read version from OBS Studio (bin\64bit\obs64.exe).', mbError, MB_OK);
Result := False;
exit;
end;
{ shift 16 bits to the right to get major version }
ObsMajorVersion := ObsMS shr 16;
{ select only low 16 bits }
ObsMinorVersion := ObsMS and $FFFF;
if ObsMajorVersion < 29 then begin
MsgBox('Version of OBS Studio (bin\64bit\obs64.exe) is lower than the version 29 required.', mbError, MB_OK);
Result := False;
exit;
end;
end;

BIN
media/aitum.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
media/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

55
media/stream.svg Normal file
View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8"
version="1.1"
viewBox="0 0 2.1166666 2.1166667"
height="8"
width="8">
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(0,-294.88332)"
id="layer1">
<circle
r="0.39687499"
cy="295.94165"
cx="1.0582843"
id="path4544-3"
style="fill:#00D29A;fill-opacity:1;stroke:none;stroke-width:0.13229169;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" />
<path
d="M 0.41577997,296.5497 A 0.79374999,0.79374999 0 0 1 0.13224262,295.94165 0.79374999,0.79374999 0 0 1 0.41577996,295.3336"
id="path4546-7"
style="fill:none;fill-opacity:1;stroke:#00D29A;stroke-width:0.15875001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" />
<path
transform="scale(-1,1)"
d="m -1.7008867,296.5497 a 0.79374999,0.79374999 0 0 1 -0.2835374,-0.60805 0.79374999,0.79374999 0 0 1 0.2835374,-0.60805"
id="path4546-1-3"
style="fill:none;fill-opacity:1;stroke:#00D29A;stroke-width:0.15875001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" />
<path
d="M 0.63239977,296.41847 A 0.62244385,0.62244385 0 0 1 0.4100551,295.94165 0.62244385,0.62244385 0 0 1 0.63239977,295.46483"
id="path4546-5-7"
style="fill:none;fill-opacity:1;stroke:#00D29A;stroke-width:0.15875001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" />
<path
transform="scale(-1,1)"
d="m -1.484193,296.41847 a 0.62244391,0.62244391 0 0 1 -0.2223447,-0.47682 0.62244391,0.62244391 0 0 1 0.2223447,-0.47682"
id="path4546-5-4-2"
style="fill:none;fill-opacity:1;stroke:#00D29A;stroke-width:0.15875001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

55
media/streaming.svg Normal file
View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8"
version="1.1"
viewBox="0 0 2.1166666 2.1166667"
height="8"
width="8">
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(0,-294.88332)"
id="layer1">
<circle
r="0.39687499"
cy="295.94165"
cx="1.0582843"
id="path4544-3"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.13229169;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" />
<path
d="M 0.41577997,296.5497 A 0.79374999,0.79374999 0 0 1 0.13224262,295.94165 0.79374999,0.79374999 0 0 1 0.41577996,295.3336"
id="path4546-7"
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.15875001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" />
<path
transform="scale(-1,1)"
d="m -1.7008867,296.5497 a 0.79374999,0.79374999 0 0 1 -0.2835374,-0.60805 0.79374999,0.79374999 0 0 1 0.2835374,-0.60805"
id="path4546-1-3"
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.15875001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" />
<path
d="M 0.63239977,296.41847 A 0.62244385,0.62244385 0 0 1 0.4100551,295.94165 0.62244385,0.62244385 0 0 1 0.63239977,295.46483"
id="path4546-5-7"
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.15875001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" />
<path
transform="scale(-1,1)"
d="m -1.484193,296.41847 a 0.62244391,0.62244391 0 0 1 -0.2223447,-0.47682 0.62244391,0.62244391 0 0 1 0.2223447,-0.47682"
id="path4546-5-4-2"
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.15875001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

554
multistream.cpp Normal file
View File

@ -0,0 +1,554 @@
#include "multistream.hpp"
#include "obs-module.h"
#include "version.h"
#include <obs-frontend-api.h>
#include <QDesktopServices>
#include <QGroupBox>
#include <QLabel>
#include <QMainWindow>
#include <QMessageBox>
#include <QPushButton>
#include <QScrollArea>
#include <QVBoxLayout>
#include <util/config-file.h>
#include <util/platform.h>
OBS_DECLARE_MODULE()
OBS_MODULE_AUTHOR("Aitum");
OBS_MODULE_USE_DEFAULT_LOCALE("aitum-multistream", "en-US")
static MultistreamDock *multistream_dock;
bool obs_module_load(void)
{
//return true;
blog(LOG_INFO, "[Aitum-Multistream] loaded version %s", PROJECT_VERSION);
const auto main_window = static_cast<QMainWindow *>(obs_frontend_get_main_window());
multistream_dock = new MultistreamDock(main_window);
obs_frontend_add_dock_by_id("AitumMultistreamDock", obs_module_text("AitumMultistream"), multistream_dock);
return true;
}
void obs_module_unload()
{
if (multistream_dock) {
delete multistream_dock;
}
}
MultistreamDock::MultistreamDock(QWidget *parent) : QFrame(parent)
{
auto l = new QVBoxLayout;
setLayout(l);
auto t = new QWidget;
auto tl = new QVBoxLayout;
t->setLayout(tl);
auto mainCanvasGroup = new QGroupBox(QString::fromUtf8(obs_module_text("MainCanvas")));
//mainCanvasGroup->setObjectName("mainCanvasGroup");
//mainCanvasGroup->setStyleSheet(QString("QGroupBox#mainCanvasGroup{background-color: %1;}") // padding-top: 4px; .arg(main_window->palette().color(QPalette::ColorRole::Mid).name(QColor::HexRgb)));
mainCanvasLayout = new QVBoxLayout;
auto mainStreamGroup = new QGroupBox;
mainStreamGroup->setStyleSheet(QString("QGroupBox{background-color: %1; padding-top: 4px;}")
.arg(palette().color(QPalette::ColorRole::Mid).name(QColor::HexRgb)));
//mainStreamGroup->setStyleSheet(QString("QGroupBox{padding-top: 4px;}"));
auto mainStreamLayout = new QVBoxLayout;
auto l2 = new QHBoxLayout;
l2->addWidget(new QLabel(QString::fromUtf8(obs_module_text("BuiltinStream"))), 1);
mainStreamButton = new QPushButton;
mainStreamButton->setMinimumHeight(30);
mainStreamButton->setObjectName(QStringLiteral("canvasStream"));
mainStreamButton->setIcon(streamInactiveIcon);
mainStreamButton->setCheckable(true);
mainStreamButton->setChecked(false);
connect(mainStreamButton, &QPushButton::clicked, [this] {
if (obs_frontend_streaming_active()) {
obs_frontend_streaming_stop();
mainStreamButton->setChecked(false);
} else {
obs_frontend_streaming_start();
mainStreamButton->setChecked(true);
}
mainStreamButton->setStyleSheet(
QString::fromUtf8(mainStreamButton->isChecked() ? "background: rgb(0,210,153);" : ""));
mainStreamButton->setIcon(mainStreamButton->isChecked() ? streamActiveIcon : streamInactiveIcon);
});
//streamButton->setSizePolicy(sp2);
mainStreamButton->setToolTip(QString::fromUtf8(obs_module_text("Stream")));
l2->addWidget(mainStreamButton);
mainStreamLayout->addLayout(l2);
mainStreamGroup->setLayout(mainStreamLayout);
mainCanvasLayout->addWidget(mainStreamGroup);
mainCanvasGroup->setLayout(mainCanvasLayout);
tl->addWidget(mainCanvasGroup);
tl->addStretch(1);
//auto verticalCanvasGroup = new QGroupBox(QString::fromUtf8(obs_module_text("VerticalCanvas")));
//tl->addWidget(verticalCanvasGroup);
QScrollArea *scrollArea = new QScrollArea;
scrollArea->setWidget(t);
scrollArea->setWidgetResizable(true);
scrollArea->setLineWidth(0);
scrollArea->setFrameShape(QFrame::NoFrame);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
l->addWidget(scrollArea, 1);
auto buttonRow = new QHBoxLayout;
buttonRow->setContentsMargins(0, 0, 0, 0);
auto configButton = new QPushButton;
configButton->setMinimumHeight(30);
configButton->setProperty("themeID", "configIconSmall");
configButton->setFlat(true);
configButton->setAutoDefault(false);
//configButton->setSizePolicy(sp2);
configButton->setToolTip(QString::fromUtf8(obs_module_text("AitumMultistreamSettings")));
QPushButton::connect(configButton, &QPushButton::clicked, [this] {
if (!configDialog)
configDialog = new OBSBasicSettings((QMainWindow *)obs_frontend_get_main_window());
auto settings = obs_data_create();
if (current_config)
obs_data_apply(settings, current_config);
configDialog->LoadSettings(settings);
configDialog->setResult(QDialog::Rejected);
if (configDialog->exec() == QDialog::Accepted) {
if (current_config) {
obs_data_apply(current_config, settings);
obs_data_release(settings);
SaveSettings();
LoadSettings();
} else {
current_config = settings;
}
} else {
obs_data_release(settings);
}
});
buttonRow->addWidget(configButton);
auto aitumButton = new QPushButton;
aitumButton->setMinimumHeight(30);
//aitumButton->setSizePolicy(sp2);
aitumButton->setIcon(QIcon(":/aitum/media/aitum.png"));
aitumButton->setToolTip(QString::fromUtf8("https://aitum.tv"));
QPushButton::connect(aitumButton, &QPushButton::clicked, [] { QDesktopServices::openUrl(QUrl("https://aitum.tv")); });
buttonRow->addWidget(aitumButton);
l->addLayout(buttonRow);
obs_frontend_add_event_callback(frontend_event, this);
}
MultistreamDock::~MultistreamDock()
{
obs_data_release(current_config);
obs_frontend_remove_event_callback(frontend_event, this);
multistream_dock = nullptr;
}
void MultistreamDock::frontend_event(enum obs_frontend_event event, void *private_data)
{
auto md = (MultistreamDock *)private_data;
if (event == OBS_FRONTEND_EVENT_PROFILE_CHANGED || event == OBS_FRONTEND_EVENT_FINISHED_LOADING) {
md->LoadSettingsFile();
} else if (event == OBS_FRONTEND_EVENT_PROFILE_CHANGING || event == OBS_FRONTEND_EVENT_PROFILE_RENAMED ||
event == OBS_FRONTEND_EVENT_EXIT) {
md->SaveSettings();
} else if (event == OBS_FRONTEND_EVENT_STREAMING_STARTING || event == OBS_FRONTEND_EVENT_STREAMING_STARTED) {
md->mainStreamButton->setChecked(true);
md->mainStreamButton->setStyleSheet(QString::fromUtf8("background: rgb(0,210,153);"));
md->mainStreamButton->setIcon(md->streamActiveIcon);
} else if (event == OBS_FRONTEND_EVENT_STREAMING_STOPPING || event == OBS_FRONTEND_EVENT_STREAMING_STOPPED) {
md->mainStreamButton->setChecked(false);
md->mainStreamButton->setStyleSheet(QString::fromUtf8(""));
md->mainStreamButton->setIcon(md->streamInactiveIcon);
}
}
void MultistreamDock::LoadSettingsFile()
{
obs_data_release(current_config);
current_config = nullptr;
char *path = obs_module_config_path("config.json");
if (!path)
return;
obs_data_t *config = obs_data_create_from_json_file_safe(path, "bak");
bfree(path);
if (!config) {
config = obs_data_create();
blog(LOG_WARNING, "[Aitum Multistream] No configuration file loaded");
} else {
blog(LOG_INFO, "[Aitum Multistream] Loaded configuration file");
}
char *profile = obs_frontend_get_current_profile();
auto profiles = obs_data_get_array(config, "profiles");
auto pc = obs_data_array_count(profiles);
obs_data_t *pd = nullptr;
for (size_t i = 0; i < pc; i++) {
obs_data_t *t = obs_data_array_item(profiles, i);
if (!t)
continue;
auto name = obs_data_get_string(t, "name");
if (strcmp(profile, name) == 0) {
pd = t;
break;
}
obs_data_release(t);
}
obs_data_array_release(profiles);
obs_data_release(config);
if (!pd) {
current_config = obs_data_create();
obs_data_set_string(current_config, "name", profile);
bfree(profile);
blog(LOG_INFO, "[Aitum Multistream] profile not found");
LoadSettings();
return;
}
bfree(profile);
current_config = pd;
LoadSettings();
}
void MultistreamDock::LoadSettings()
{
auto outputs = obs_data_get_array(current_config, "outputs");
auto count = obs_data_array_count(outputs);
int idx = 1;
while (auto item = mainCanvasLayout->itemAt(idx)) {
auto streamGroup = item->widget();
auto name = streamGroup->objectName();
bool found = false;
for (size_t i = 0; i < count; i++) {
auto item = obs_data_array_item(outputs, i);
if (QString::fromUtf8(obs_data_get_string(item, "name")) == name) {
found = true;
}
obs_data_release(item);
}
if (!found) {
if (streamGroup->layout()) {
while (QLayoutItem *item = streamGroup->layout()->takeAt(0)) {
delete item->widget();
delete item->layout();
delete item;
}
delete streamGroup->layout();
}
mainCanvasLayout->removeWidget(streamGroup);
delete streamGroup;
} else {
idx++;
}
}
obs_data_array_enum(
outputs,
[](obs_data_t *data, void *param) {
auto d = (MultistreamDock *)param;
d->LoadOutput(data);
},
this);
obs_data_array_release(outputs);
}
void MultistreamDock::LoadOutput(obs_data_t *data)
{
auto name = QString::fromUtf8(obs_data_get_string(data, "name"));
for (int i = 1; i < mainCanvasLayout->count(); i++) {
auto item = mainCanvasLayout->itemAt(i);
if (item->widget()->objectName() == name) {
return;
}
}
auto streamGroup = new QGroupBox;
streamGroup->setStyleSheet(QString("QGroupBox{background-color: %1; padding-top: 4px;}")
.arg(palette().color(QPalette::ColorRole::Mid).name(QColor::HexRgb)));
streamGroup->setObjectName(name);
//mainStreamGroup->setStyleSheet(QString("QGroupBox{padding-top: 4px;}"));
auto streamLayout = new QVBoxLayout;
auto l2 = new QHBoxLayout;
l2->addWidget(new QLabel(name), 1);
auto streamButton = new QPushButton;
streamButton->setMinimumHeight(30);
streamButton->setObjectName(QStringLiteral("canvasStream"));
streamButton->setIcon(streamInactiveIcon);
streamButton->setCheckable(true);
streamButton->setChecked(false);
connect(streamButton, &QPushButton::clicked, [this, streamButton, data] {
if (streamButton->isChecked()) {
if (!StartOutput(data, streamButton))
streamButton->setChecked(false);
} else {
}
streamButton->setStyleSheet(QString::fromUtf8(streamButton->isChecked() ? "background: rgb(0,210,153);" : ""));
streamButton->setIcon(streamButton->isChecked() ? streamActiveIcon : streamInactiveIcon);
});
//streamButton->setSizePolicy(sp2);
streamButton->setToolTip(QString::fromUtf8(obs_module_text("Stream")));
l2->addWidget(streamButton);
streamLayout->addLayout(l2);
streamGroup->setLayout(streamLayout);
mainCanvasLayout->addWidget(streamGroup);
}
static void ensure_directory(char *path)
{
#ifdef _WIN32
char *backslash = strrchr(path, '\\');
if (backslash)
*backslash = '/';
#endif
char *slash = strrchr(path, '/');
if (slash) {
*slash = 0;
os_mkdirs(path);
*slash = '/';
}
#ifdef _WIN32
if (backslash)
*backslash = '\\';
#endif
}
void MultistreamDock::SaveSettings()
{
char *path = obs_module_config_path("config.json");
if (!path)
return;
obs_data_t *config = obs_data_create_from_json_file_safe(path, "bak");
if (!config) {
ensure_directory(path);
config = obs_data_create();
blog(LOG_WARNING, "[Aitum Multistream] New configuration file");
}
auto profiles = obs_data_get_array(config, "profiles");
if (!profiles) {
profiles = obs_data_array_create();
obs_data_set_array(config, "profiles", profiles);
}
obs_data_t *pd = nullptr;
if (current_config) {
auto old_name = obs_data_get_string(current_config, "name");
auto pc = obs_data_array_count(profiles);
for (size_t i = 0; i < pc; i++) {
obs_data_t *t = obs_data_array_item(profiles, i);
if (!t)
continue;
auto name = obs_data_get_string(t, "name");
if (strcmp(old_name, name) == 0) {
pd = t;
break;
}
obs_data_release(t);
}
}
if (!pd) {
pd = obs_data_create();
obs_data_array_push_back(profiles, pd);
}
obs_data_array_release(profiles);
char *profile = obs_frontend_get_current_profile();
obs_data_set_string(pd, "name", profile);
bfree(profile);
if (current_config)
obs_data_apply(pd, current_config);
obs_data_release(pd);
if (obs_data_save_json_safe(config, path, "tmp", "bak")) {
blog(LOG_INFO, "[Aitum Multistream] Saved settings");
} else {
blog(LOG_ERROR, "[Aitum Multistream] Failed saving settings");
}
obs_data_release(config);
bfree(path);
}
bool MultistreamDock::StartOutput(obs_data_t *settings, QPushButton *streamButton)
{
const char *name = obs_data_get_string(settings, "name");
auto old = outputs.find(name);
if (old != outputs.end()) {
auto service = obs_output_get_service(old->second);
if (obs_output_active(old->second)) {
obs_output_stop(old->second);
}
obs_output_release(old->second);
obs_service_release(service);
outputs.erase(old);
}
obs_encoder_t *venc = nullptr;
obs_encoder_t *aenc = nullptr;
auto advanced = obs_data_get_bool(settings, "advanced");
if (advanced) {
auto venc_name = obs_data_get_string(settings, "video_encoder");
if (!venc_name || venc_name[0] == '\0') {
//use main encoder
auto main_output = obs_frontend_get_streaming_output();
venc = obs_output_get_video_encoder2(main_output, obs_data_get_int(settings, "video_encoder_index"));
if (!venc || !obs_output_active(main_output)) {
obs_output_release(main_output);
QMessageBox::warning(this, QString::fromUtf8(obs_module_text("MainOutputNotActive")),
QString::fromUtf8(obs_module_text("MainOutputNotActive")));
return false;
}
obs_output_release(main_output);
} else {
obs_data_t *s = nullptr;
auto ves = obs_data_get_obj(settings, "video_encoder_settings");
if (ves) {
s = obs_data_create();
obs_data_apply(s, ves);
obs_data_release(ves);
}
venc = obs_video_encoder_create(venc_name, name, s, nullptr);
obs_data_release(s);
obs_encoder_set_video(venc, obs_get_video());
auto divisor = obs_data_get_int(settings, "frame_rate_divisor");
if (divisor > 1)
obs_encoder_set_frame_rate_divisor(venc, divisor);
bool scale = obs_data_get_bool(settings, "scale");
if (scale) {
obs_encoder_set_scaled_size(venc, obs_data_get_int(settings, "width"), obs_data_get_int(settings, "height"));
obs_encoder_set_gpu_scale_type(venc, (obs_scale_type)obs_data_get_int(settings, "scale_type"));
}
}
auto aenc_name = obs_data_get_string(settings, "audio_encoder");
if (!aenc_name || aenc_name[0] == '\0') {
//use main encoder
auto main_output = obs_frontend_get_streaming_output();
aenc = obs_output_get_audio_encoder(main_output, obs_data_get_int(settings, "audio_encoder_index"));
if (!aenc || !obs_output_active(main_output)) {
obs_output_release(main_output);
QMessageBox::warning(this, QString::fromUtf8(obs_module_text("MainOutputNotActive")),
QString::fromUtf8(obs_module_text("MainOutputNotActive")));
return false;
}
obs_output_release(main_output);
} else {
obs_data_t *s = nullptr;
auto aes = obs_data_get_obj(settings, "audio_encoder_settings");
if (aes) {
s = obs_data_create();
obs_data_apply(s, aes);
obs_data_release(aes);
}
aenc = obs_audio_encoder_create(venc_name, name, s, obs_data_get_int(settings, "audio_track"), nullptr);
obs_data_release(s);
obs_encoder_set_audio(aenc, obs_get_audio());
}
} else {
auto main_output = obs_frontend_get_streaming_output();
venc = main_output ? obs_output_get_video_encoder(main_output) : nullptr;
if (!venc || !obs_output_active(main_output)) {
obs_output_release(main_output);
QMessageBox::warning(this, QString::fromUtf8(obs_module_text("MainOutputNotActive")),
QString::fromUtf8(obs_module_text("MainOutputNotActive")));
return false;
}
aenc = obs_output_get_audio_encoder(main_output, 0);
obs_output_release(main_output);
}
if (!aenc || !venc) {
return false;
}
auto s = obs_data_create();
obs_data_set_string(s, "server", obs_data_get_string(settings, "server"));
obs_data_set_string(s, "key", obs_data_get_string(settings, "key"));
//use_auth
//username
//password
auto service = obs_service_create("rtmp_custom", name, s, nullptr);
obs_data_release(s);
#if LIBOBS_API_VER < MAKE_SEMANTIC_VERSION(29, 1, 0)
const char *type = obs_service_get_output_type(service);
#else
const char *type = obs_service_get_preferred_output_type(service);
#endif
if (!type) {
#if LIBOBS_API_VER < MAKE_SEMANTIC_VERSION(29, 1, 0)
const char *url = obs_service_get_url(service);
#else
const char *url = obs_service_get_connect_info(service, OBS_SERVICE_CONNECT_INFO_SERVER_URL);
#endif
type = "rtmp_output";
if (url != NULL && strncmp(url, "ftl", 3) == 0) {
type = "ftl_output";
} else if (url != NULL && strncmp(url, "rtmp", 4) != 0) {
type = "ffmpeg_mpegts_muxer";
}
}
auto output = obs_output_create(type, name, nullptr, nullptr);
obs_output_set_service(output, service);
config_t *config = obs_frontend_get_profile_config();
if (config) {
obs_data_t *output_settings = obs_data_create();
obs_data_set_string(output_settings, "bind_ip", config_get_string(config, "Output", "BindIP"));
obs_data_set_string(output_settings, "ip_family", config_get_string(config, "Output", "IPFamily"));
obs_output_update(output, output_settings);
obs_data_release(output_settings);
bool useDelay = config_get_bool(config, "Output", "DelayEnable");
int delaySec = config_get_int(config, "Output", "DelaySec");
bool preserveDelay = config_get_bool(config, "Output", "DelayPreserve");
obs_output_set_delay(output, useDelay ? delaySec : 0, preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0);
}
signal_handler_t *signal = obs_output_get_signal_handler(output);
//signal_handler_disconnect(signal, "start", stream_output_start, streamButton);
//signal_handler_disconnect(signal, "stop", stream_output_stop, streamButton);
signal_handler_connect(signal, "start", stream_output_start, streamButton);
signal_handler_connect(signal, "stop", stream_output_stop, streamButton);
//for (size_t i = 0; i < MAX_OUTPUT_VIDEO_ENCODERS; i++) {
//auto venc = obs_output_get_video_encoder2(main_output, 0);
//for (size_t i = 0; i < MAX_OUTPUT_AUDIO_ENCODERS; i++) {
//obs_output_get_audio_encoder(main_output, 0);
obs_output_set_video_encoder(output, venc);
obs_output_set_audio_encoder(output, aenc, 0);
obs_output_start(output);
outputs[obs_data_get_string(settings, "name")] = output;
return true;
}
void MultistreamDock::stream_output_start(void *data, calldata_t *calldata)
{
UNUSED_PARAMETER(calldata);
auto streamButton = (QPushButton *)data;
streamButton->setChecked(true);
}
void MultistreamDock::stream_output_stop(void *data, calldata_t *calldata)
{
auto streamButton = (QPushButton *)data;
const char *last_error = (const char *)calldata_ptr(calldata, "last_error");
streamButton->setChecked(false);
}

44
multistream.hpp Normal file
View File

@ -0,0 +1,44 @@
#pragma once
#include <QFrame>
#include <QVBoxLayout>
#include <QPushButton>
#include <obs.h>
#include "config-dialog.hpp"
class OBSBasicSettings;
class MultistreamDock : public QFrame {
Q_OBJECT
private:
OBSBasicSettings *configDialog = nullptr;
obs_data_t *current_config = nullptr;
QVBoxLayout *mainCanvasLayout = nullptr;
QPushButton *mainStreamButton = nullptr;
std::map<std::string, obs_output_t *> outputs;
void LoadSettingsFile();
void LoadSettings();
void LoadOutput(obs_data_t *data);
void SaveSettings();
bool StartOutput(obs_data_t *settings, QPushButton *streamButton);
QIcon streamActiveIcon = QIcon(":/aitum/media/streaming.svg");
QIcon streamInactiveIcon = QIcon(":/aitum/media/stream.svg");
static void frontend_event(enum obs_frontend_event event, void *private_data);
static void stream_output_stop(void *data, calldata_t *calldata);
static void stream_output_start(void *data, calldata_t *calldata);
public:
MultistreamDock(QWidget *parent = nullptr);
~MultistreamDock();
};

32
resource.rc.in Normal file
View File

@ -0,0 +1,32 @@
1 VERSIONINFO
FILEVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0
PRODUCTVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0
FILEFLAGSMASK 0x0L
#ifdef _DEBUG
FILEFLAGS 0x1L
#else
FILEFLAGS 0x0L
#endif
FILEOS 0x0L
FILETYPE 0x2L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", "Aitum"
VALUE "FileDescription", "${PROJECT_FULL_NAME}"
VALUE "FileVersion", "${PROJECT_VERSION}"
VALUE "InternalName", "${PROJECT_NAME}"
VALUE "LegalCopyright", "(C) Aitum"
VALUE "OriginalFilename", "${PROJECT_NAME}"
VALUE "ProductName", "${PROJECT_FULL_NAME}"
VALUE "ProductVersion", "${PROJECT_VERSION}"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END

7
resources.qrc Normal file
View File

@ -0,0 +1,7 @@
<RCC>
<qresource prefix="/aitum">
<file>media/aitum.png</file>
<file>media/stream.svg</file>
<file>media/streaming.svg</file>
</qresource>
</RCC>

6
version.h.in Normal file
View File

@ -0,0 +1,6 @@
#pragma once
#define PROJECT_VERSION "${PROJECT_VERSION}"
#define PROJECT_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}
#define PROJECT_VERSION_MINOR ${PROJECT_VERSION_MINOR}
#define PROJECT_VERSION_PATCH ${PROJECT_VERSION_PATCH}