2026-09-18 OBS开源第一个版本

This commit is contained in:
evan.liu 2026-09-18 06:08:44 +08:00
commit 438b2bada3
5148 changed files with 747947 additions and 0 deletions

213
obs-studio/.clang-format Normal file
View File

@ -0,0 +1,213 @@
# please use clang-format version 16 or later
AccessModifierOffset: -8
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Left
AlignOperands: true
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: false
AllowAllConstructorInitializersOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Inline
AllowShortIfStatementsOnASingleLine: false
AllowShortLambdasOnASingleLine: Inline
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: false
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: true
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Custom
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: BeforeColon
BreakStringLiterals: false # apparently unpredictable
ColumnLimit: 120
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 8
ContinuationIndentWidth: 8
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
FixNamespaceComments: true
ForEachMacros:
- 'json_object_foreach'
- 'json_object_foreach_safe'
- 'json_array_foreach'
- 'HASH_ITER'
IncludeBlocks: Preserve
IndentCaseLabels: false
IndentPPDirectives: None
IndentWidth: 8
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: true
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 8
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
PenaltyBreakAssignment: 10
PenaltyBreakBeforeFirstCallParameter: 30
PenaltyBreakComment: 10
PenaltyBreakFirstLessLess: 0
PenaltyBreakString: 10
PenaltyExcessCharacter: 100
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Right
ReflowComments: false
SkipMacroDefinitionBody: true
SortIncludes: false
SortUsingDeclarations: false
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInCStyleCastParentheses: false
SpacesInContainerLiterals: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
StatementMacros:
- 'Q_OBJECT'
TabWidth: 8
TypenameMacros:
- 'DARRAY'
UseTab: ForContinuationAndIndentation
---
Language: Cpp
Standard: c++17
InsertBraces: true
---
Language: ObjC
AccessModifierOffset: 2
AlignArrayOfStructures: Right
AlignConsecutiveAssignments: None
AlignConsecutiveBitFields: None
AlignConsecutiveDeclarations: None
AlignConsecutiveMacros:
Enabled: true
AcrossEmptyLines: false
AcrossComments: true
AllowShortBlocksOnASingleLine: Never
AllowShortEnumsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: None
AttributeMacros: ['__unused', '__autoreleasing', '_Nonnull', '__bridge']
BitFieldColonSpacing: Both
#BreakBeforeBraces: Webkit
BreakBeforeBraces: Custom
BraceWrapping:
AfterCaseLabel: false
AfterClass: true
AfterControlStatement: Never
AfterEnum: false
AfterFunction: true
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
BeforeLambdaBody: false
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: false
SplitEmptyRecord: false
SplitEmptyNamespace: true
BreakAfterAttributes: Never
BreakArrays: false
BreakBeforeConceptDeclarations: Allowed
BreakBeforeInlineASMColon: OnlyMultiline
BreakConstructorInitializers: AfterColon
BreakInheritanceList: AfterComma
ColumnLimit: 120
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: LogicalBlock
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
IndentAccessModifiers: false
IndentCaseBlocks: false
IndentCaseLabels: true
IndentExternBlock: Indent
IndentGotoLabels: false
IndentRequiresClause: true
IndentWidth: 4
IndentWrappedFunctionNames: true
InsertBraces: false
InsertNewlineAtEOF: true
KeepEmptyLinesAtTheStartOfBlocks: false
LambdaBodyIndentation: Signature
NamespaceIndentation: All
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 4
ObjCBreakBeforeNestedBlockParam: false
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
PPIndentWidth: -1
PackConstructorInitializers: NextLine
QualifierAlignment: Leave
ReferenceAlignment: Right
RemoveSemicolon: false
RequiresClausePosition: WithPreceding
RequiresExpressionIndentation: OuterScope
SeparateDefinitionBlocks: Leave
ShortNamespaceLines: 1
SortIncludes: false
#SortUsingDeclarations: LexicographicNumeric
SortUsingDeclarations: true
SpaceAfterCStyleCast: true
SpaceAfterLogicalNot: false
SpaceAroundPointerQualifiers: Default
SpaceBeforeCaseColon: false
SpaceBeforeCpp11BracedList: true
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 2
SpacesInConditionalStatement: false
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: -1
Standard: c++17
TabWidth: 4
UseTab: Never
...

63
obs-studio/.editorconfig Normal file
View File

@ -0,0 +1,63 @@
# EditorConfig is awesome: http://EditorConfig.org
# Since OBS follows the Linux kernel coding style I have started this file to
# help with automatically setting various text editors to those requirements.
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file.
[*]
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
indent_style = tab
indent_size = 8
# As per notr1ch, for 3rd party code that's a part of obs-outputs.
[plugins/obs-outputs/librtmp/*.{cpp,c,h}]
indent_style = space
indent_size = 4
[CMakeLists.txt]
indent_style = space
indent_size = 2
[**/CMakeLists.txt]
indent_style = space
indent_size = 2
[cmake/**/*.cmake]
indent_style = space
indent_size = 2
[plugins/{rtmp-services,win-capture}/data/**/*.json]
indent_style = space
indent_size = 4
[*.qss]
indent_style = space
indent_size = 4
[build-aux/**/*.json]
indent_style = space
indent_size = 4
[*.py]
indent_style = space
indent_size = 4
[*.yaml]
indent_style = space
indent_size = 2
[{*.zsh,.*.zsh,build-aux/.functions/*,.github/scripts/utils.zsh/*}]
indent_style = space
indent_size = 2
[*.ui]
indent_style = space
indent_size = 1
[{*.obt,*.oha,*.ovt}]
indent_style = space
indent_size = 4

8
obs-studio/.gersemirc Normal file
View File

@ -0,0 +1,8 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/BlankSpruce/gersemi/master/gersemi/configuration.schema.json
definitions: []
line_length: 120
indent: 2
list_expansion: favour-inlining
unsafe: false
warn_about_unknown_commands: false

2
obs-studio/.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,2 @@
open_collective: obsproject
patreon: obsproject

View File

@ -0,0 +1,121 @@
name: Set Up and Build obs-studio
description: Builds obs-studio for specified architecture and build config
inputs:
target:
description: Build target for obs-studio
required: true
config:
description: Build configuration
required: false
default: RelWithDebInfo
codesign:
description: Enable codesigning (macOS only)
required: false
default: 'false'
codesignIdent:
description: Developer ID for application codesigning (macOS only)
required: false
default: '-'
codesignTeam:
description: Team ID for application codesigning (macOS only)
required: false
default: ''
provisioningProfileUUID:
description: UUID of provisioning profile (macOS only)
required: false
default: ''
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 --no-rcs --errexit --pipefail {0}
working-directory: ${{ inputs.workingDirectory }}
env:
CODESIGN_IDENT: ${{ inputs.codesignIdent }}
CODESIGN_TEAM: ${{ inputs.codesignTeam }}
PROVISIONING_PROFILE: ${{ inputs.provisioningProfileUUID }}
run: |
: Run macOS Build
local -a build_args=(
--config ${{ inputs.config }}
--target macos-${{ inputs.target }}
)
if (( ${+RUNNER_DEBUG} )) build_args+=(--debug)
if [[ '${{ inputs.codesign }}' == true ]] build_args+=(--codesign)
git fetch origin --no-tags --no-recurse-submodules -q
.github/scripts/build-macos ${build_args}
- name: Install Dependencies 🛍️
if: runner.os == 'Linux'
shell: bash
run: |
: Install Dependencies 🛍️
echo ::group::Install Dependencies
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH
brew update
brew install --quiet zsh
echo ::endgroup::
- name: Run Ubuntu Build
if: runner.os == 'Linux'
shell: zsh --no-rcs --errexit --pipefail {0}
working-directory: ${{ inputs.workingDirectory }}
run: |
: Run Ubuntu Build
local -a build_args=(
--config ${{ inputs.config }}
--target ubuntu-${{ inputs.target }}
)
if (( ${+RUNNER_DEBUG} )) build_args+=(--debug)
git fetch origin --no-tags --no-recurse-submodules -q
.github/scripts/build-ubuntu ${build_args}
- name: Run Windows Build
if: runner.os == 'Windows'
shell: pwsh
working-directory: ${{ inputs.workingDirectory }}
run: |
# Run Windows Build
$BuildArgs = @{
Target = '${{ inputs.target }}'
Configuration = '${{ inputs.config }}'
}
if ( $Env:RUNNER_DEBUG -ne $null ) {
$BuildArgs += @{ Debug = $true }
}
git fetch origin --no-tags --no-recurse-submodules -q
.github/scripts/Build-Windows.ps1 @BuildArgs
- name: Create Summary 📊
if: runner.os == 'Linux'
shell: zsh --no-rcs --errexit --pipefail {0}
run: |
: Create Summary 📊
local -a ccache_data
if (( ${+RUNNER_DEBUG} )) {
setopt XTRACE
ccache_data=("${(fA)$(ccache -s -vv)}")
} else {
ccache_data=("${(fA)$(ccache -s)}")
}
print '### ${{ runner.os }} Ccache Stats (${{ inputs.target }})' >> $GITHUB_STEP_SUMMARY
print '```' >> $GITHUB_STEP_SUMMARY
for line (${ccache_data}) {
print ${line} >> $GITHUB_STEP_SUMMARY
}
print '```' >> $GITHUB_STEP_SUMMARY

View File

@ -0,0 +1,82 @@
name: Check For Changed Files
description: Checks for changed files compared to specific git reference and glob expression
inputs:
baseRef:
description: Git reference to check against
required: false
ref:
description: Git reference to check with
required: false
default: HEAD
checkGlob:
description: Glob expression to limit check to specific files
required: false
useFallback:
description: Use fallback compare against prior commit
required: false
default: 'true'
diffFilter:
description: git diff-filter string to use
required: false
default: ''
outputs:
hasChangedFiles:
value: ${{ steps.checks.outputs.hasChangedFiles }}
description: True if specified files were changed in comparison to specified git reference
changedFiles:
value: ${{ steps.checks.outputs.changedFiles }}
description: List of changed files
runs:
using: composite
steps:
- name: Check For Changed Files ✅
shell: bash
id: checks
env:
GIT_BASE_REF: ${{ inputs.baseRef }}
GIT_REF: ${{ inputs.ref }}
GITHUB_EVENT_FORCED: ${{ github.event.forced }}
GITHUB_REF_BEFORE: ${{ github.event.before }}
USE_FALLBACK: ${{ inputs.useFallback }}
DIFF_FILTER: ${{ inputs.diffFilter }}
run: |
: Check for Changed Files ✅
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
shopt -s extglob
shopt -s dotglob
# 4b825dc642cb6eb9a060e54bf8d69288fbee4904 is a "hidden" sha1 hash of
# the "empty tree", retrieved via 'git hash-object -t tree /dev/null',
# and used here as a last-resort fallback to always provide a valid
# git ref.
if [[ "${GIT_BASE_REF}" ]]; then
if ! git cat-file -e "${GIT_BASE_REF}" &> /dev/null; then
echo "::warning::Provided base reference ${GIT_BASE_REF} is invalid"
if [[ "${USE_FALLBACK}" == 'true' ]]; then
GIT_BASE_REF='HEAD~1'
fi
fi
else
if ! git cat-file -e ${GITHUB_REF_BEFORE} &> /dev/null; then
GITHUB_REF_BEFORE='4b825dc642cb6eb9a060e54bf8d69288fbee4904'
fi
GIT_BASE_REF='HEAD~1'
case "${GITHUB_EVENT_NAME}" in
pull_request) GIT_BASE_REF="origin/${GITHUB_BASE_REF}" ;;
push) if [[ "${GITHUB_EVENT_FORCED}" != 'true' ]]; then GIT_BASE_REF="${GITHUB_REF_BEFORE}"; fi ;;
*) ;;
esac
fi
changes=($(git diff --name-only --diff-filter="${DIFF_FILTER}" ${GIT_BASE_REF} ${GIT_REF} -- ${{ inputs.checkGlob }}))
if (( ${#changes[@]} )); then
file_string="${changes[*]}"
echo "hasChangedFiles=true" >> $GITHUB_OUTPUT
echo "changedFiles=[\"${file_string// /\",\"}\"]" >> $GITHUB_OUTPUT
else
echo "hasChangedFiles=false" >> $GITHUB_OUTPUT
echo "changedFiles=[]" >> GITHUB_OUTPUT
fi

View File

@ -0,0 +1,64 @@
name: Compatibility Data Validator
description: Checks Windows compatibility data files
inputs:
repositorySecret:
description: GitHub token for API access
required: true
workingDirectory:
description: Working directory for checks
required: false
default: ${{ github.workspace }}
runs:
using: composite
steps:
- name: Check Runner Operating System 🏃‍♂️
if: runner.os == 'Windows'
shell: bash
run: |
: Check Runner Operating System 🏃‍♂️
echo "services-validation action requires a macOS-based or Linux-based runner."
exit 2
- name: Install and Configure Python 🐍
shell: bash
run: |
: Install and Configure Python 🐍
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
echo ::group::Python Set Up
if [[ "${RUNNER_OS}" == Linux ]]; then
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH
fi
brew update
brew install --quiet python3
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install jsonschema json_source_map
echo ::endgroup::
- name: Validate Compatibility Files JSON Schema 🕵️
shell: bash
working-directory: ${{ inputs.workingDirectory }}
run: |
: Validate services file JSON schema 🕵️
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
shopt -s extglob
echo ::group::Schema Validation
source .venv/bin/activate
python3 -u \
.github/scripts/utils.py/check-jsonschema.py \
--loglevel INFO \
plugins/win-capture/data/@(compatibility|package).json
echo ::endgroup::
- name: Annotate Schema Validation Errors 🏷️
uses: yuzutech/annotations-action@00b2d488bcba3bd01014dc073d276ef4a45d5c6c # v0.6.0
if: failure()
with:
repo-token: ${{ inputs.repositorySecret }}
title: Compatibility JSON Errors
input: ${{ inputs.workingDirectory }}/validation_errors.json

View File

@ -0,0 +1,75 @@
name: Run flatpak-builder-lint
description: Runs flatpak-builder-lint with exceptions
inputs:
artifact:
description: Type of artifact to lint (builddir, repo, manifest, appstream)
required: true
path:
description: Path to flatpak-builder manifest or Flatpak build directory
required: true
workingDirectory:
description: Working directory to clone flatpak-builder-lint
required: false
default: ${{ github.workspace }}
runs:
using: composite
steps:
- name: Check artifact type
shell: bash
working-directory: ${{ inputs.workingDirectory }}
run: |
: Check artifact input
if ! [[ "${{ inputs.artifact }}" =~ builddir|repo|manifest|appstream ]]; then
echo "::error::Given artifact type is incorrect"
exit 2
fi
- name: Run flatpak-builder-lint
id: result
shell: bash
working-directory: ${{ inputs.workingDirectory }}
run: |
: Run flatpak-builder-lint
return=0
result="$(flatpak-builder-lint --exceptions --user-exceptions ${GITHUB_ACTION_PATH}/exceptions.json ${{ inputs.artifact }} ${{ inputs.path }})" || return=$?
if [[ ${return} != 0 && -z "${result}" ]]; then
echo "::error::Error while running flatpak-builder-lint"
exit 2
fi
if [[ "${{ inputs.artifact }}" == "appstream" ]]; then
echo "${result}"
if [[ ${return} != 0 ]]; then echo "::error::Flatpak appstream info is not valid"; fi
exit ${return}
fi
# This jq command selects any available array under the 'warnings' key in the JSON document
# or provides an empty array as a fallback if the key is not present. This array is then
# piped to the 'map' function to apply a transformation to every element in the array,
# converting it to a string prefixed with the output level, the actual element value, and
# finally the suffix string defined in 'template'.
#
# The result of this expression is concatenated with a similar expression doing the same
# but for the 'errors' key and its associated array.
#
# The second jq invocation then selects each element of the array and outputs it directly,
# which will be strings of the formats:
#
# * '::warning:: <original warning> <template text>'
# * '::error:: <original error> <template text>'
#
# If no warnings or errors were found, only empty arrays were used for 'map' and thus
# only an empty result array is generated.
template=" found in the Flatpak ${{ inputs.artifact }}"
while read -r log_line; do
if [[ "${log_line}" ]]; then echo "${log_line}"; fi
done <<< "$(echo "${result}" \
| jq -r '(.warnings // [] | try map("::warning:: " + . + "${template}")) + (.errors // [] | try map("::error:: " + . + "${template}"))' \
| jq -r '.[]')"
exit ${return}

View File

@ -0,0 +1,8 @@
{
"com.obsproject.Studio": [
"finish-args-flatpak-spawn-access",
"external-gitmodule-url-found",
"finish-args-host-filesystem-access",
"appstream-flathub-manifest-url-not-reachable"
]
}

View File

@ -0,0 +1,38 @@
name: Flatpak Manifest Validator
description: Checks order of Flatpak modules in manifest file
inputs:
manifestFile:
description: Flatpak manifest file to check
failCondition:
description: Controls whether failed checks also fail the workflow run
required: false
default: never
workingDirectory:
description: Working directory for checks
required: false
default: ${{ github.workspace }}
runs:
using: composite
steps:
- name: Check Runner Operating System 🏃‍♂️
if: runner.os == 'Windows'
shell: bash
run: |
: Check Runner Operating System 🏃‍♂️
echo "services-validation action requires a macOS-based or Linux-based runner."
exit 2
- name: Validate Flatpak Manifest 🕵️
shell: bash
working-directory: ${{ inputs.workingDirectory }}
run: |
: Validate Flatpak Manifest 🕵️
echo ::group::Run Validation
if [[ '${{ inputs.failCondition }}' == 'never' ]]; then set +e; fi
python3 -u \
build-aux/format-manifest.py \
build-aux/com.obsproject.Studio.json \
--check \
--loglevel INFO
echo ::endgroup::

View File

@ -0,0 +1,74 @@
name: Generate Documentation
description: Updates Sphinx-based documentation
inputs:
sourceDirectory:
description: Path to repository checkout
required: false
default: ${{ github.workspace }}
disableLinkExtensions:
description: Disable Sphinx link extensions
required: false
default: 'false'
runs:
using: composite
steps:
- name: Update Version Number and Copyright ↗️
id: setup
shell: bash
run: |
: Update Version Number and Copyright ↗️
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
: "${major:=}"
: "${minor:=}"
: "${patch:=}"
# This expression will first try to match all the LIBOBS_API_[...]_VER
# lines in 'obs-config.h', before removing the '#define ' prefix and
# trimming away whitespace characters and linebreaks.
# This will yield a single line with the each major, minor, and patch
# version variable name followed by its value, so every even item
# in this string will contain a version part.
read -r _ major _ minor _ patch _ <<< \
"$(grep -E -e "#define LIBOBS_API_(MAJOR|MINOR|PATCH)_VER *" libobs/obs-config.h \
| sed 's/#define //g' \
| tr -s ' ' \
| tr '\n' ' ')"
# This expression simply replaces the definition of the 'version' and
# 'release' variables in the Python script with updated variants using
# the version tokens set by the previous expression.
# The copyright variable assignment is updated to use the current
# local year as the second year value.
sed -i -E \
-e "s/version = '([0-9]+\.[0-9]+\.[0-9]+)'/version = '${major}.${minor}.${patch}'/g" \
-e "s/release = '([0-9]+\.[0-9]+\.[0-9]+)'/release = '${major}.${minor}.${patch}'/g" \
-e "s/copyright = '(2017-[0-9]+, Lain Bailey)'/copyright = '2017-$(date +"%Y"), Lain Bailey'/g" \
${{ inputs.sourceDirectory }}/docs/sphinx/conf.py
if [[ '${{ inputs.disableLinkExtensions }}' == 'true' ]]; then
sed -i -e "s/html_link_suffix = None/html_link_suffix = ''/g" \
${{ inputs.sourceDirectory }}/docs/sphinx/conf.py
echo "artifactName=OBS Studio Docs (No Extensions)" >> $GITHUB_OUTPUT
else
echo "artifactName=OBS Studio Docs" >> $GITHUB_OUTPUT
fi
echo "commitHash=${GITHUB_SHA:0:9}" >> $GITHUB_OUTPUT
- name: Install Sphinx 📜
uses: totaldebug/sphinx-publish-action@a44364271972de3b13c5b63801c2596dfde82413
with:
sphinx_src: ${{ inputs.sourceDirectory }}/docs/sphinx
build_only: true
target_branch: master
target_path: '../home/_build'
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ steps.setup.outputs.artifactName }} ${{ steps.setup.outputs.commitHash }}
path: |
${{ runner.temp }}/_github_home/_build
!${{ runner.temp }}/_github_home/_build/.doctrees

View File

@ -0,0 +1,115 @@
name: Package obs-studio
description: Packages obs-studio 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: '-'
codesignUser:
description: Apple ID username for notarization (macOS only)
required: false
default: ''
codesignPass:
description: Apple ID password for notarization (macOS only)
required: false
default: ''
package:
description: Create platform-specific packages instead of archives
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 --no-rcs --errexit --pipefail {0}
working-directory: ${{ inputs.workingDirectory }}
env:
CODESIGN_IDENT: ${{ inputs.codesignIdent }}
CODESIGN_IDENT_USER: ${{ inputs.codesignUser }}
CODESIGN_IDENT_PASS: ${{ inputs.codesignPass }}
run: |
: Run macOS Packaging
local -a package_args=(
--target macos-${{ inputs.target }}
--config ${{ inputs.config }}
)
if (( ${+RUNNER_DEBUG} )) build_args+=(--debug)
if [[ '${{ inputs.codesign }}' == true ]] package_args+=(--codesign)
if [[ '${{ inputs.notarize }}' == true ]] package_args+=(--notarize)
if [[ '${{ inputs.package }}' == true ]] package_args+=(--package)
.github/scripts/package-macos ${package_args}
- name: Install Dependencies 🛍️
if: runner.os == 'Linux'
shell: bash
run: |
: Install Dependencies 🛍️
echo ::group::Install Dependencies
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH
brew update
brew install --quiet zsh
echo ::endgroup::
- name: Run Ubuntu packaging
if: runner.os == 'Linux'
shell: zsh --no-rcs --errexit --pipefail {0}
working-directory: ${{ inputs.workingDirectory }}
run: |
: Run Ubuntu Packaging
local -a package_args=(
--target ubuntu-${{ inputs.target }}
--config ${{ inputs.config }}
)
if (( ${+RUNNER_DEBUG} )) build_args+=(--debug)
if [[ '${{ inputs.package }}' == true ]] package_args+=(--package)
${{ inputs.workingDirectory }}/.github/scripts/package-ubuntu ${package_args}
- name: Run Windows packaging
if: runner.os == 'Windows'
shell: pwsh
working-directory: ${{ inputs.workingDirectory }}
run: |
# Run Windows Packaging
$PackageArgs = @{
Target = '${{ inputs.target }}'
Configuration = '${{ inputs.config }}'
}
if ( $Env:RUNNER_DEBUG -ne $null ) {
$PackageArgs += @{ Debug = $true }
}
if ( ( Test-Path env:CI ) -and ( Test-Path env:RUNNER_DEBUG ) ) {
$BuildArgs += @{
Debug = $true
}
}
.github/scripts/Package-windows.ps1 @PackageArgs

View File

@ -0,0 +1,61 @@
name: Validate UI XML
description: Validates Qt UI XML files
inputs:
failCondition:
description: Controls whether failed checks also fail the workflow run
required: false
default: never
workingDirectory:
description: Working directory for checks
required: false
default: ${{ github.workspace }}
runs:
using: composite
steps:
- name: Check Runner Operating System 🏃‍♂️
if: runner.os == 'Windows'
shell: bash
run: |
: Check Runner Operating System 🏃‍♂️
echo "::notice::qt-xml-validator action requires an Linux-based or macOS-based runner."
exit 2
- name: Install xmllint 🕵️
if: runner.os == 'Linux'
shell: bash
run: |
: Install xmllint 🕵️
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
echo ::group::Installing libxml2-utils
sudo apt-get -qq update
sudo apt-get install --no-install-recommends -y libxml2-utils
echo ::endgroup::
- name: Register Annotations 📝
uses: korelstar/xmllint-problem-matcher@dd2ad21bd8a2de0187cb621419537f345e7e509c # v1.3.0
- name: Check for Changed Files ✅
uses: ./.github/actions/check-changes
id: checks
with:
checkGlob: 'frontend/forms/**/*.ui'
- name: Validate XML 💯
if: fromJSON(steps.checks.outputs.hasChangedFiles)
id: result
shell: bash
env:
CHANGED_FILES: ${{ steps.checks.outputs.changedFiles }}
run: |
: Validate XML 💯
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
CHANGED_FILES=($(echo "${CHANGED_FILES//[\[\]\'\"]/}" | tr "," "\n"))
if (( ${#CHANGED_FILES[@]} )); then
if [[ '${{ inputs.failCondition }}' == never ]]; then set +e; fi
xmllint \
--schema ${{ github.workspace }}/frontend/forms/XML-Schema-Qt5.15.xsd \
--noout "${CHANGED_FILES[@]}"
fi

View File

@ -0,0 +1,63 @@
name: Run clang-format
description: Runs clang-format and checks for any changes introduced by it
inputs:
failCondition:
description: Controls whether failed checks also fail the workflow run
required: false
default: never
workingDirectory:
description: Working directory for checks
required: false
default: ${{ github.workspace }}
runs:
using: composite
steps:
- name: Check Runner Operating System 🏃‍♂️
if: runner.os == 'Windows'
shell: bash
run: |
: Check Runner Operating System 🏃‍♂️
echo "::notice::run-clang-format action requires a macOS-based or Linux-based runner."
exit 2
- name: Check for Changed Files ✅
uses: ./.github/actions/check-changes
id: checks
with:
checkGlob: "'*.c' '*.h' '*.cpp' '*.hpp' '*.m' '*.mm'"
diffFilter: 'ACM'
- name: Install Dependencies 🛍️
if: runner.os == 'Linux' && fromJSON(steps.checks.outputs.hasChangedFiles)
shell: bash
run: |
: Install Dependencies 🛍️
echo ::group::Install Dependencies
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH
echo "/home/linuxbrew/.linuxbrew/opt/clang-format@22/bin" >> $GITHUB_PATH
brew update
brew install --quiet zsh
echo ::endgroup::
- name: Run clang-format 🐉
if: fromJSON(steps.checks.outputs.hasChangedFiles)
id: result
shell: zsh --no-rcs --errexit --pipefail {0}
working-directory: ${{ inputs.workingDirectory }}
env:
CHANGED_FILES: ${{ steps.checks.outputs.changedFiles }}
run: |
: Run clang-format 🐉
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
print ::group::Install clang-format-22
brew update
brew trust obsproject/tools
brew install --quiet obsproject/tools/clang-format@22
print ::endgroup::
print ::group::Run clang-format-22
local -a changes=(${(s:,:)CHANGED_FILES//[\[\]\'\"]/})
./build-aux/run-clang-format --fail-${{ inputs.failCondition }} --check ${changes}
print ::endgroup::

View File

@ -0,0 +1,62 @@
name: Run gersemi
description: Runs gersemi and checks for any changes introduced by it
inputs:
failCondition:
description: Controls whether failed checks also fail the workflow run
required: false
default: never
workingDirectory:
description: Working directory for checks
required: false
default: ${{ github.workspace }}
runs:
using: composite
steps:
- name: Check Runner Operating System 🏃‍♂️
if: runner.os == 'Windows'
shell: bash
run: |
: Check Runner Operating System 🏃‍♂️
echo "::notice::run-gersemi action requires a macOS-based or Linux-based runner."
exit 2
- name: Check for Changed Files ✅
uses: ./.github/actions/check-changes
id: checks
with:
checkGlob: "'*.cmake' '*CMakeLists.txt'"
diffFilter: 'ACM'
- name: Install Dependencies 🛍️
if: runner.os == 'Linux' && fromJSON(steps.checks.outputs.hasChangedFiles)
shell: bash
run: |
: Install Dependencies 🛍️
echo ::group::Install Dependencies
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH
brew update
brew install --quiet zsh
echo ::endgroup::
- name: Run gersemi 🎛️
if: fromJSON(steps.checks.outputs.hasChangedFiles)
id: result
shell: zsh --no-rcs --errexit --pipefail {0}
working-directory: ${{ github.workspace }}
env:
CHANGED_FILES: ${{ steps.checks.outputs.changedFiles }}
run: |
: Run gersemi 🎛️
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
print ::group::Install gersemi
brew update
brew trust obsproject/tools
brew install --quiet obsproject/tools/gersemi@0.25
print ::endgroup::
print ::group::Run gersemi
local -a changes=(${(s:,:)CHANGED_FILES//[\[\]\'\"]/})
./build-aux/run-gersemi --fail-${{ inputs.failCondition }} --check ${changes}
print ::endgroup::

View File

@ -0,0 +1,61 @@
name: Run swift-format
description: Runs swift-format and checks for any changes introduced by it
inputs:
failCondition:
description: Controls whether failed checks also fail the workflow run
required: false
default: never
workingDirectory:
description: Working directory for checks
required: false
default: ${{ github.workspace }}
runs:
using: composite
steps:
- name: Check Runner Operating System 🏃‍♂️
if: runner.os == 'Windows'
shell: bash
run: |
: Check Runner Operating System 🏃‍♂️
echo "::notice::run-swift-format action requires a macOS-based or Linux-based runner."
exit 2
- name: Check for Changed Files ✅
uses: ./.github/actions/check-changes
id: checks
with:
checkGlob: "'*.swift'"
diffFilter: 'ACM'
- name: Install Dependencies 🛍️
if: runner.os == 'Linux' && fromJSON(steps.checks.outputs.hasChangedFiles)
shell: bash
run: |
: Install Dependencies 🛍️
echo ::group::Install Dependencies
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH
brew update
brew install --quiet zsh
echo ::endgroup::
- name: Run swift-format 🔥
if: fromJSON(steps.checks.outputs.hasChangedFiles)
id: result
shell: zsh --no-rcs --errexit --pipefail {0}
working-directory: ${{ github.workspace }}
env:
CHANGED_FILES: ${{ steps.checks.outputs.changedFiles }}
run: |
: Run swift-format 🔥
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
print ::group::Install swift-format
brew update
brew install --quiet swift-format
print ::endgroup::
print ::group::Run swift-format
local -a changes=(${(s:,:)CHANGED_FILES//[\[\]\'\"]/})
./build-aux/run-swift-format --fail-${{ inputs.failCondition }} --check ${changes}
print ::endgroup::

View File

@ -0,0 +1,122 @@
name: Services Validation
description: Checks services configuration file and checks for defunct services
inputs:
repositorySecret:
description: GitHub token for API access
required: true
checkApiSecret:
description: Token for server check API
required: false
checkApiServers:
description: Servers for the check API
required: false
runSchemaChecks:
description: Enable schema checking
required: false
default: 'true'
runServiceChecks:
description: Enable defunct service checking
required: false
default: 'false'
createPullRequest:
description: Enable pull request creation after service checks
required: false
default: 'false'
workingDirectory:
description: Working directory for checks
required: false
default: ${{ github.workspace }}
outputs:
hasDefunctServices:
description: True if defunct services were found in configuration
value: ${{ steps.check.outputs.make_pr }}
runs:
using: composite
steps:
- name: Check Runner Operating System 🏃‍♂️
if: runner.os == 'Windows'
shell: bash
run: |
: Check Runner Operating System 🏃‍♂️
echo "::notice::services-validation action requires a macOS-based or Linux-based runner."
exit 2
- name: Install and Configure Python 🐍
shell: bash
run: |
: Configure Python 🐍
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install jsonschema json_source_map requests aiohttp
echo ::endgroup::
- name: Validate Services File JSON Schema 🕵️
if: fromJSON(inputs.runSchemaChecks)
shell: bash
working-directory: ${{ inputs.workingDirectory }}
run: |
: Validate Services File JSON Schema 🕵️
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
shopt -s extglob
echo ::group::Run Validation
source .venv/bin/activate
python3 -u \
.github/scripts/utils.py/check-jsonschema.py \
plugins/rtmp-services/data/@(services|package).json \
--loglevel INFO
echo ::endgroup::
- name: Annotate schema validation errors 🏷️
if: fromJSON(inputs.runSchemaChecks) && failure()
uses: yuzutech/annotations-action@00b2d488bcba3bd01014dc073d276ef4a45d5c6c # v0.6.0
with:
repo-token: ${{ inputs.repositorySecret }}
title: Service JSON Errors
input: ${{ inputs.workingDirectory }}/validation_errors.json
- name: Restore Timestamp Cache ⏳
if: fromJSON(inputs.runServiceChecks)
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ github.workspace }}/other
key: service-check-${{ github.run_id }}
restore-keys: service-check-
- name: Check for defunct services 📉
id: services-check
if: fromJSON(inputs.runServiceChecks)
shell: bash
working-directory: ${{ inputs.workingDirectory }}
env:
GITHUB_TOKEN: ${{ inputs.repositorySecret }}
WORKFLOW_RUN_ID: ${{ github.run_id }}
REPOSITORY: ${{ github.repository }}
API_KEY: ${{ inputs.checkApiSecret }}
API_SERVERS: ${{ inputs.checkApiServers }}
run: |
: Check for defunct services 📉
source .venv/bin/activate
python3 -u .github/scripts/utils.py/check-services.py
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: fromJSON(inputs.runServiceChecks)
with:
name: timestamps
path: ${{ inputs.workingDirectory }}/other/*
- name: Create pull request 🔧
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
if: fromJSON(inputs.createPullRequest) && fromJSON(inputs.runServiceChecks) && fromJSON(steps.services-check.outputs.make_pr)
with:
author: 'Service Checker <commits@obsproject.com>'
commit-message: 'rtmp-services: Remove defunct servers/services'
title: 'rtmp-services: Remove defunct servers/services'
branch: 'automated/clean-services'
body: ${{ fromJSON(steps.services-check.outputs.pr_message) }}
delete-branch: true

View File

@ -0,0 +1,151 @@
name: Set up macOS Code Signing
description: Sets up code signing certificates, provisioning profiles, and notarization information
inputs:
codesignIdentity:
description: Code signing identity
required: true
codesignCertificate:
description: PKCS12 certificate in base64 format
required: true
certificatePassword:
description: Password required to install PKCS12 certificate
required: true
keychainPassword:
description: Password to use for temporary keychain
required: false
notarizationUser:
description: Apple ID to use for notarization
required: false
notarizationPassword:
description: Application password for notarization
provisioningProfile:
description: Provisioning profile in base64 format
required: false
outputs:
haveCodesignIdent:
description: True if necessary code signing credentials were found
value: ${{ steps.codesign.outputs.haveCodesignIdent }}
haveProvisioningProfile:
description: True if necessary provisioning profile credentials were found
value: ${{ steps.provisioning.outputs.haveProvisioningProfile || steps.codesign.outputs.haveProvisioningProfile }}
provisioningProfileUUID:
description: UUID of imported provisioning profile
value: ${{ steps.provisioning.outputs.provisioningProfileUUID }}
haveNotarizationUser:
description: True if necessary notarization credentials were found
value: ${{ steps.notarization.outputs.haveNotarizationUser || steps.codesign.outputs.haveNotarizationUser }}
codesignIdent:
description: Code signing identity
value: ${{ steps.codesign.outputs.codesignIdent }}
codesignTeam:
description: Code signing team
value: ${{ steps.codesign.outputs.codesignTeam }}
runs:
using: composite
steps:
- name: Check Runner Operating System 🏃‍♂️
if: runner.os != 'macOS'
shell: bash
run: |
: Check Runner Operating System 🏃‍♂️
echo "setup-macos-codesigning action requires a macOS-based runner."
exit 2
- name: macOS Code Signing ✍️
id: codesign
shell: zsh --no-rcs --errexit --pipefail {0}
env:
MACOS_SIGNING_IDENTITY: ${{ inputs.codesignIdentity }}
MACOS_SIGNING_CERT: ${{ inputs.codesignCertificate }}
MACOS_SIGNING_CERT_PASSWORD: ${{ inputs.certificatePassword }}
MACOS_KEYCHAIN_PASSWORD: ${{ inputs.keychainPassword }}
run: |
: macOS Code Signing ✍️
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
if [[ ${MACOS_SIGNING_IDENTITY} && ${MACOS_SIGNING_CERT} ]] {
print 'haveCodesignIdent=true' >> $GITHUB_OUTPUT
local -r certificate_path="${RUNNER_TEMP}/build_certificate.p12"
local -r keychain_path="${RUNNER_TEMP}/app-signing.keychain-db"
print -n "${MACOS_SIGNING_CERT}" | base64 --decode --output=${certificate_path}
: "${MACOS_KEYCHAIN_PASSWORD:="$(print ${RANDOM} | shasum | head -c 32)"}"
print '::group::Keychain setup'
security create-keychain -p "${MACOS_KEYCHAIN_PASSWORD}" ${keychain_path}
security set-keychain-settings -lut 21600 ${keychain_path}
security unlock-keychain -p "${MACOS_KEYCHAIN_PASSWORD}" ${keychain_path}
security import "${certificate_path}" -P "${MACOS_SIGNING_CERT_PASSWORD}" -A \
-t cert -f pkcs12 -k ${keychain_path} \
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/xcrun
security set-key-partition-list -S 'apple-tool:,apple:' -k "${MACOS_KEYCHAIN_PASSWORD}" \
${keychain_path} &> /dev/null
security list-keychain -d user -s ${keychain_path} 'login-keychain'
print '::endgroup::'
local -r team_id="${${MACOS_SIGNING_IDENTITY##* }//(\(|\))/}"
print "codesignIdent=${MACOS_SIGNING_IDENTITY}" >> $GITHUB_OUTPUT
print "MACOS_KEYCHAIN_PASSWORD=${MACOS_KEYCHAIN_PASSWORD}" >> $GITHUB_ENV
print "codesignTeam=${team_id}" >> $GITHUB_OUTPUT
} else {
print 'haveCodesignIdent=false' >> $GITHUB_OUTPUT
print 'haveProvisioningProfile=false' >> $GITHUB_OUTPUT
print 'haveNotarizationUser=false' >> $GITHUB_OUTPUT
}
- name: Provisioning Profile 👤
id: provisioning
if: fromJSON(steps.codesign.outputs.haveCodesignIdent)
shell: zsh --no-rcs --errexit --pipefail {0}
env:
MACOS_SIGNING_PROVISIONING_PROFILE: ${{ inputs.provisioningProfile }}
run: |
: Provisioning Profile 👤
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
if [[ "${MACOS_SIGNING_PROVISIONING_PROFILE}" ]] {
print 'haveProvisioningProfile=true' >> $GITHUB_OUTPUT
local -r profile_path="${RUNNER_TEMP}/build_profile.provisionprofile"
print -n "${MACOS_SIGNING_PROVISIONING_PROFILE}" \
| base64 --decode --output="${profile_path}"
print '::group::Provisioning Profile Setup'
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
security cms -D -i ${profile_path} -o ${RUNNER_TEMP}/build_profile.plist
local -r uuid="$(plutil -extract UUID raw ${RUNNER_TEMP}/build_profile.plist)"
local -r team_id="$(plutil -extract TeamIdentifier.0 raw -expect string ${RUNNER_TEMP}/build_profile.plist)"
if [[ ${team_id} != '${{ steps.codesign.outputs.codesignTeam }}' ]] {
print '::notice::Code Signing team in provisioning profile does not match certificate.'
}
cp ${profile_path} ~/Library/MobileDevice/Provisioning\ Profiles/${uuid}.provisionprofile
print "provisioningProfileUUID=${uuid}" >> $GITHUB_OUTPUT
print '::endgroup::'
} else {
print 'haveProvisioningProfile=false' >> $GITHUB_OUTPUT
}
- name: Notarization 🧑‍💼
id: notarization
if: fromJSON(steps.codesign.outputs.haveCodesignIdent)
shell: zsh --no-rcs --errexit --pipefail {0}
env:
MACOS_NOTARIZATION_USERNAME: ${{ inputs.notarizationUser }}
MACOS_NOTARIZATION_PASSWORD: ${{ inputs.notarizationPassword }}
run: |
: Notarization 🧑‍💼
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
if [[ ${MACOS_NOTARIZATION_USERNAME} && ${MACOS_NOTARIZATION_PASSWORD} ]] {
print 'haveNotarizationUser=true' >> $GITHUB_OUTPUT
} else {
print 'haveNotarizationUser=false' >> $GITHUB_OUTPUT
}

View File

@ -0,0 +1,247 @@
name: Generate Sparkle Appcast
description: Creates Sparkle Appcast for a new release and generates delta patch files
inputs:
sparklePrivateKey:
description: Private key used for Sparkle signing
required: true
baseImage:
description: Disk image to base the Sparkle Appcast on
required: true
channel:
description: Sparkle Appcast channel to use
required: false
default: stable
count:
description: Number of old versions to generate deltas for
required: false
default: '1'
urlPrefix:
description: URL prefix to use for Sparkle downloads
required: true
customTitle:
description: Custom title to use for Appcast
required: false
customLink:
description: Custom link to use for Appcast
required: false
runs:
using: composite
steps:
- name: Check Runner Operating System 🏃‍♂️
if: runner.os != 'macOS'
shell: bash
run: |
: Check Runner Operating System 🏃‍♂️
echo '::notice::sparkle-appcast action requires a macOS-based runner.'
exit 2
- name: Install Dependencies
shell: zsh --no-rcs --errexit --pipefail {0}
run: |
: Install Dependencies
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
print ::group::Install Dependencies
brew update
brew install --quiet coreutils pandoc
print ::endgroup::
- name: Set Up Sparkle ✨
shell: zsh --no-rcs --errexit --pipefail {0}
run: |
: Set Up Sparkle ✨
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
local version
local base_url
local hash
IFS=';' read -r version base_url hash <<< \
"$(jq -r '
.configurePresets[]
| select(.name=="dependencies")
| .vendor["obsproject.com/obs-studio"].tools.sparkle
| {version, baseUrl, hash}
| join(";")
' CMakePresets.json)"
mkdir -p Sparkle && pushd Sparkle
curl -s -L -O "${base_url}/${version}/Sparkle-${version}.tar.xz"
local checksum="$(sha256sum Sparkle-${version}.tar.xz | cut -d " " -f 1)"
if [[ ${hash} != ${checksum} ]] {
print "::error::Sparkle-${version}.tar.xz checksum mismatch: ${checksum} (expected: ${hash})"
exit 2
}
tar -xJf "Sparkle-${version}.tar.xz"
popd
mkdir builds
mkdir -p output/appcasts/stable
- name: Download Builds 📥
id: builds
shell: zsh --no-rcs --errexit --pipefail {0}
run: |
: Download Builds 📥
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
pushd builds
local image_location=(${{ inputs.baseImage }})
hdiutil attach -readonly -noverify -noautoopen -plist ${image_location} > result.plist
local -i num_entities=$(( $(plutil -extract system-entities raw -- result.plist) - 1 ))
local keys
local mount_point
for i ({0..${num_entities}}) {
keys=($(plutil -extract system-entities.${i} raw -- result.plist))
if [[ ${keys} == *mount-point* ]] {
mount_point=$(plutil -extract system-entities.${i}.mount-point raw -- result.plist)
break
}
}
local feed_url
local info_plist=(${mount_point}/*.app/Contents/Info.plist)
if [[ -f ${info_plist} ]] {
feed_url=$(plutil -extract SUFeedURL raw -- ${info_plist})
} else {
print '::error:: No Info.plist file found in specified disk image.'
hdiutil detach ${mount_point}
exit 2
}
print "feedUrl=${feed_url}" >> $GITHUB_OUTPUT
hdiutil detach ${mount_point}
curl -s -L -O ${feed_url}
# The Xpath Xplained:
#
# //rss/channel/item - Select every <item> node, under a
# <channel> node, under a <rss> node,
# which:
# [*...] - Has a child node, which
# [local-name()='channel'] - Has the local name "channel"
# (required to match the
# namespaced sparkle:channel node),
# which in turn has
# [text()='<channel>'] - A text node that contains the
# content of inputs.channel
# /enclosure/@url - Then select the "url" attribute of
# every <enclosure> node under
# these matching <item> nodes
local -a artifacts=($(\
xmllint \
-xpath "//rss/channel/item[*[local-name()='channel'][text()='${{ inputs.channel }}']]/enclosure/@url" \
${feed_url:t} \
| sed -n 's/.*url="\(.*\)"/\1/p')
)
local url
local file_name
for i ({1..${{ inputs.count }}}) {
url="${artifacts[${i}]}"
file_name="${artifacts[${i}]:t}"
curl -s -L -O ${url}
}
mv ${{ inputs.baseImage }} ${PWD}
rm -rf - result.plist
popd
- name: Prepare Release Notes 📝
shell: zsh --no-rcs --errexit --pipefail {0}
run: |
: Prepare Release Notes 📝
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
git tag -l --format='%(contents:subject)' ${GITHUB_REF_NAME} > notes.rst
echo '###################################################' >> notes.rst
echo '' >> notes.rst
git tag -l --format='%(contents:body)' ${GITHUB_REF_NAME} >> notes.rst
pandoc -f rst -t html notes.rst -o output/appcasts/notes_${{ inputs.channel }}.html
- name: Generate Appcast 🎙️
shell: zsh --no-rcs --errexit --pipefail {0}
run: |
: Generate Appcast 🎙️
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
print -n '${{ inputs.sparklePrivateKey }}' >> eddsa_private.key
local feed_url='${{ steps.builds.outputs.feedUrl }}'
local arch=${${${(s:_:)feed_url:t}[2]}//x86/x86_64}
local sparkle_options=(
--verbose
--ed-key-file eddsa_private.key
--download-url-prefix '${{ inputs.urlPrefix }}/'
--full-release-notes-url "${feed_url//updates_*/notes_${{ inputs.channel }}.html}"
--maximum-versions 0
--maximum-deltas '${{ inputs.count }}'
--channel '${{ inputs.channel }}'
)
if [[ '${{ inputs.channel }}' == 'stable' ]] {
sparkle_options+=(--phased-rollout-interval 86400)
}
Sparkle/bin/generate_appcast \
${sparkle_options} \
builds
local -a deltas=(builds/*.delta(N))
if (( #deltas )) {
mkdir -p output/sparkle_deltas/${arch}
mv ${deltas} output/sparkle_deltas/${arch}
}
mv builds/*.xml output/appcasts
- name: Adjust Appcast 🎙️
shell: zsh --no-rcs --errexit --pipefail {0}
run: |
: Adjust Appcast 🎙️
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
local feed_url='${{ steps.builds.outputs.feedUrl }}'
local arch=${${${(s:_:)feed_url:t}[2]}//x86/x86_64}
local -a appcasts=(output/appcasts/*_v2.xml)
local adjusted
for appcast (${appcasts}) {
adjusted="${appcast//.xml/-adjusted.xml}"
xsltproc \
--stringparam pDeltaUrl "${{ inputs.urlPrefix }}/sparkle_deltas/${arch}/" \
--stringparam pSparkleUrl '${{ inputs.urlPrefix }}/' \
--stringparam pCustomTitle '${{ inputs.customTitle }}' \
--stringparam pCustomLink '${{ inputs.customLink }}' \
-o ${adjusted} ${GITHUB_ACTION_PATH}/appcast_adjust.xslt ${appcast}
xmllint --format ${adjusted} >! ${appcast}
rm ${adjusted}
}
- name: Create Legacy Appcast 📟
shell: zsh --no-rcs --errexit --pipefail {0}
run: |
: Create Legacy Appcast 📟
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
local -a appcasts=(output/appcasts/*_v2.xml)
local legacy
local feed_url='${{ steps.builds.outputs.feedUrl }}'
local arch=${${${(s:_:)feed_url:t}[2]}//x86/x86_64}
for appcast (${appcasts}) {
legacy="${appcast//.xml/-legacy.xml}"
xsltproc \
-o ${legacy} ${GITHUB_ACTION_PATH}/appcast_legacy.xslt ${appcast}
xmllint --format ${legacy} >! output/appcasts/stable/${${appcast:t}//_v2.xml/.xml}
if [[ ${arch} == x86_64 ]] xmllint --format ${legacy} >! output/appcasts/stable/${${appcast:t}//_x86_64_v2.xml/.xml}
rm ${legacy}
}

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<xsl:output method="xml" encoding="UTF-8" omit-xml-declaration="no"/>
<xsl:strip-space elements="*"/>
<!-- Select /rss/channel/title and store it as default value for 'pCustomTitle' -->
<xsl:param name="pCustomTitle" select="/rss/channel/title" />
<!-- Select /rss/channel/link and store it as default value for 'pCustomLink' -->
<xsl:param name="pCustomLink" select="/rss/channel/link" />
<!-- Set empty strings as default values for pSparkleUrl and pDeltaUrl -->
<xsl:param name="pSparkleUrl" select="''" />
<xsl:param name="pDeltaUrl" select="''" />
<!-- XSLT identity rule - copy all nodes and their child nodes as well as attributes
(attributes are _not_ descendants of the nodes they belong to).
This copy rule is applied as the "default" transformation for all nodes.
-->
<xsl:template match="@* | node()">
<xsl:copy>
<xsl:apply-templates select="@* | node()" />
</xsl:copy>
</xsl:template>
<!-- Match the <title> under <channel> and <rss> and do not translate it
(effectively removes it).
-->
<xsl:template match="/rss/channel/title" />
<!-- Match the <link> under <channel> and <rss> and do not translate it
(effectively removes it).
-->
<xsl:template match="/rss/channel/link" />
<!-- Match the <channel> under <rss> and apply a copy translation, which
* Creates a new <title> element with a text child node and the text content
of the pCustomTitle variable
* Creates a new <link> element with a text child node and the text content
of the pCustomLink variable
* Copies all child nodes and attributes of the original <channel> node
(this is why <title> and <link> were explicitly not translated before)
-->
<xsl:template match="/rss/channel">
<xsl:copy>
<xsl:element name="title"><xsl:value-of select="$pCustomTitle" /></xsl:element>
<xsl:element name="link"><xsl:value-of select="$pCustomLink" /></xsl:element>
<xsl:apply-templates select="@* | node()" />
</xsl:copy>
</xsl:template>
<!-- Match every url attribute of <enclosure> nodes in <sparkle:deltas> nodes
(which themselves are under <item>, <channel>, and <rss> nodes respectively).
Create a new attribute with the name "url" and then set its content to either
* The original value of the attribute if it starts with the value of the
pDeltaUrl variable, OR
* The actual value of the pDeltaUrl variable, with the value of the
pSparkleUrl variable removed in front of the current url value added after
This effectively updates every url attribute on a delta <enclosure> node
that does not start with the current delta url path.
-->
<xsl:template match="/rss/channel/item/sparkle:deltas/enclosure/@url">
<xsl:attribute name="url">
<xsl:choose>
<xsl:when test="starts-with(., $pDeltaUrl)">
<xsl:value-of select="." />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$pDeltaUrl" />
<xsl:value-of select="substring-after(., $pSparkleUrl)" />
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
</xsl:template>
<!-- Match any <sparkle:fullReleaseNotesLink> node under <item>, <channel>,
and <rss> respectively, and replace it with a new node named
<sparkle:releaseNotesLink> and populate it with all child nodes and
attributes of the original node.
-->
<xsl:template match="/rss/channel/item/sparkle:fullReleaseNotesLink">
<xsl:element name="sparkle:releaseNotesLink"><xsl:apply-templates select="@* | node()" /></xsl:element>
</xsl:template>
</xsl:stylesheet>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<xsl:output method="xml" encoding="UTF-8" omit-xml-declaration="no"/>
<xsl:strip-space elements="*"/>
<!-- XSLT identity rule - copy all nodes and their child nodes as well as attributes
(attributes are _not_ descendants of the nodes they belong to).
This copy rule is applied as the "default" transformation for all nodes.
-->
<xsl:template match="@* | node()">
<xsl:copy>
<xsl:apply-templates select="@* | node()" />
</xsl:copy>
</xsl:template>
<!-- Select every <item> node under a <channel> and <rss> node respectively,
which has a <sparkle:channel> child node whose text child node's value
is not equal to 'stable', then apply to translation, effectively removing
it.
-->
<xsl:template match="/rss/channel/item[sparkle:channel[text()!='stable']]" />
<!-- Select every <sparkle:channel> node under a <item> node which sits under a
<channel> and <rss> node respectively, then apply no translation, effectively
removing it.
-->
<xsl:template match="/rss/channel/item/sparkle:channel" />
<!-- Select every <sparkle:deltas> node under a <item> node which sits under a
<channel> and <rss> node respectively, then apply no translation, effectively
removing it.
-->
<xsl:template match="/rss/channel/item/sparkle:deltas" />
</xsl:stylesheet>

View File

@ -0,0 +1,291 @@
name: Steam Upload
description: Creates and uploads stable and nightly builds of obs-studio and beta builds (if available)
inputs:
steamSecret:
description: Steam auth code
required: true
steamUser:
description: Steam user name
required: true
steamPassword:
description: Steam user password
required: true
workflowSecret:
description: GitHub API token to use for API calls
required: true
tagName:
description: Tag name to use for packaging
required: false
default: ''
stableBranch:
description: Name of the stable branch to use
required: false
default: staging
betaBranch:
description: Name of the beta branch to use
required: false
default: beta_staging
nightlyBranch:
description: Name of the nightly branch to use
required: false
default: nightly
playtestBranch:
description: Name of the playtest branch to use
required: false
default: staging
customAssetWindows:
description: Custom asset for Windows
required: false
default: ''
customAssetMacOSApple:
description: Custom asset for macOS Apple Silicon
required: false
default: ''
customAssetMacOSIntel:
description: Custom asset for macOS Intel
required: false
default: ''
preview:
description: Enable preview mode (no uploads done)
required: false
default: ''
runs:
using: composite
steps:
- name: Check Runner Operating System 🏃‍♂️
if: runner.os != 'macOS'
shell: bash
run: |
: Check Runner Operating System 🏃‍♂️
echo '::error::steam-upload action requires a macOS-based runner.'
exit 2
- name: Check GitHub Event 🔬
if: contains(fromJSON('["release", "workflow_dispatch", "schedule"]'), github.event_name) != true
shell: zsh --no-rcs --errexit --pipefail {0}
run: |
: Check GitHub Event 🔬
print "::error:steam-upload action can only be used with 'release', 'workflow-dispatch', or 'schedule' events."
exit 2
- name: Download Nightly Assets 🌙
id: asset-info-nightly
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
if: github.event_name == 'schedule'
- name: Download Assets 📥
id: asset-info
shell: zsh --no-rcs --errexit --pipefail {0}
env:
GH_TOKEN: ${{ inputs.workflowSecret }}
windows_custom_asset: ${{ inputs.customAssetWindows }}
macos_apple_custom_asset: ${{ inputs.customAssetMacOSApple }}
macos_intel_custom_asset: ${{ inputs.customAssetMacOSIntel }}
run: |
: Download Assets 📥
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
local root_dir="${PWD}"
local description
local is_prerelease
case ${GITHUB_EVENT_NAME} {
release)
gh release download ${{ inputs.tagName }} \
--pattern '*.dmg' \
--pattern '*.zip' \
--clobber
IFS=';' read -r is_prerelease description <<< \
"$(gh release view ${{ inputs.tagName }} --json isPrerelease,tagName --jq 'join(";")')"
;;
workflow_dispatch)
if [[ '${{ inputs.tagName }}' =~ [0-9]+\.[0-9]+\.[0-9]+(-(rc|beta)[0-9]+)*$ ]] {
gh release download ${{ inputs.tagName }} \
--pattern '*.dmg' \
--pattern '*.zip' \
--clobber
description='${{ inputs.tagName }}'
read -r is_prerelease <<< \
"$(gh release view ${{ inputs.tagName }} --json isPrerelease --jq '.isPrerelease')"
local -A custom_assets=(
windows "Windows x64;${windows_custom_asset}"
macos_apple "macOS Apple;${macos_apple_custom_asset}"
macos_intel "macOS Intel;${macos_intel_custom_asset}"
)
local display_name
local url
mkdir -p custom_assets && pushd custom_assets
for platform (windows macos_apple macos_intel) {
IFS=';' read -r display_name url <<< "${custom_assets[${platform}]}"
if [[ ${url} ]] {
print "::group::Download of ${display_name} custom asset"
curl --location --silent --remote-name ${url}
if [[ ! -f ${root_dir}/${url:t} ]] {
print "::warning::Custom asset for ${display_name} does not replace an existing release asset"
} else {
rm -rf -- ${root_dir}/${url:t}
}
mv ${url:t} ${root_dir}
print '::endgroup::'
}
}
popd
} else {
print "::error::Invalid tag name for non-release workflow run: '${{ inputs.tagName }}'."
exit 2
}
;;
schedule)
local short_hash="${GITHUB_SHA:0:9}"
mv obs-studio-windows-x64-${short_hash}/obs-studio-*-windows-x64.zip \
${root_dir}
mv obs-studio-macos-arm64-${short_hash}/obs-studio-*-macos-apple.dmg \
${root_dir}
mv obs-studio-macos-x86_64-${short_hash}/obs-studio-*-macos-intel.dmg \
${root_dir}
description="g${GITHUB_SHA:0:9}"
is_prerelease='false'
;;
}
print "description=${description}" >> $GITHUB_OUTPUT
print "is_prerelease=${is_prerelease}" >> $GITHUB_OUTPUT
- name: Prepare Builds for Steam 🍜
shell: zsh --no-rcs --errexit --pipefail --extendedglob {0}
run: |
: Prepare Builds for Steam 🍜
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
local root_dir="${PWD}"
mkdir -p steam && pushd steam
print '::group::Prepare Windows x64 assets'
mkdir -p steam-windows && pushd steam-windows
unzip ${root_dir}/(#i)obs-studio-*-windows-x64.zip
rm ${root_dir}/(#i)obs-studio-*-windows*.zip
cp -r ${root_dir}/build-aux/steam/scripts_windows scripts
touch disable_updater
popd
print '::endgroup::'
print '::group::Prepare macOS Apple assets'
mkdir -p steam-macos/arm64/OBS.app
hdiutil attach -noverify -readonly -noautoopen -mountpoint /Volumes/obs-studio-arm64 ${root_dir}/(#i)obs-studio-*-macos-apple.dmg
ditto /Volumes/obs-studio-arm64/OBS.app steam-macos/arm64/OBS.app
hdiutil unmount /Volumes/obs-studio-arm64
rm ${root_dir}/(#i)obs-studio-*-macos-apple.dmg
print '::endgroup::'
print '::group::Prepare macOS Intel assets'
mkdir -p steam-macos/x86_64/OBS.app
hdiutil attach -noverify -readonly -noautoopen -mountpoint /Volumes/obs-studio-x86_64 ${root_dir}/(#i)obs-studio-*-macos-intel.dmg
ditto /Volumes/obs-studio-x86_64/OBS.app steam-macos/x86_64/OBS.app
hdiutil unmount /Volumes/obs-studio-x86_64
rm ${root_dir}/(#i)obs-studio-*-macos-intel.dmg
print '::endgroup::'
cp ${root_dir}/build-aux/steam/scripts_macos/launch.sh steam-macos/launch.sh
popd
- name: Set Up steamcmd 🚂
uses: andriivitiv/setup-steamcmd@afc45f145b95c175c3a3862d3ca84756537d48e8 # v1.3.0
- name: Generate Steam auth code 🔐
id: steam-totp
uses: andriivitiv/steam-totp@1661caebf0a84a79be0166020f81837039137101 # v1.2.0
if: ${{ ! fromJSON(inputs.preview) }}
with:
shared_secret: ${{ inputs.steamSecret }}
- name: Upload to Steam 📤
shell: zsh --no-rcs --errexit --pipefail {0}
run: |
: Upload to Steam 📤
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
local root_dir="${PWD}"
local build_file='build.vdf'
local branch_name
pushd steam
print '::group::Prepare Steam Build Script'
case ${GITHUB_EVENT_NAME} {
schedule) branch_name='${{ inputs.nightlyBranch }}' ;;
release|workflow_dispatch)
if [[ '${{ steps.asset-info.outputs.is_prerelease }}' == 'true' ]] {
branch_name='${{ inputs.betaBranch }}'
} else {
branch_name='${{ inputs.stableBranch }}'
}
;;
}
sed "s/@@DESC@@/${branch_name}-${{ steps.asset-info.outputs.description }}/;s/@@BRANCH@@/${branch_name}/" \
${root_dir}/build-aux/steam/obs_build.vdf > ${build_file}
print "Generated ${build_file}:\n$(<${build_file})"
print '::endgroup::'
print '::group::Upload to Steam'
local preview='${{ inputs.preview }}'
steamcmd \
+login '${{ inputs.steamUser }}' '${{ inputs.steamPassword }}' '${{ steps.steam-totp.outputs.code }}' \
+run_app_build ${preview:+-preview} ${build_file:a} \
+quit
print '::endgroup'
popd
- name: Generate Steam auth code 🔐
id: steam-totp-playtest
uses: andriivitiv/steam-totp@1661caebf0a84a79be0166020f81837039137101 # v1.2.0
if: ${{ ! fromJSON(inputs.preview) && fromJSON(steps.asset-info.outputs.is_prerelease) }}
with:
shared_secret: ${{ inputs.steamSecret }}
- name: Upload to Steam (Playtest) 📤
if: fromJSON(steps.asset-info.outputs.is_prerelease)
shell: zsh --no-rcs --errexit --pipefail {0}
run: |
: Upload to Steam Playtest 📤
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
local root_dir="${PWD}"
local build_file='build_playtest.vdf'
local branch_name='${{ inputs.playtestBranch }}'
pushd steam
print '::group::Prepare Steam Build Script'
sed "s/@@DESC@@/${branch_name}-${{ steps.asset-info.outputs.description }}/;s/@@BRANCH@@/${branch_name}/" \
${root_dir}/build-aux/steam/obs_playtest_build.vdf > ${build_file}
print "Generated ${build_file}:\n$(<${build_file})"
print '::endgroup::'
print '::group::Upload to Steam'
local preview
if [[ '${{ inputs.preview }}' == 'true' ]] preview='-preview'
steamcmd \
+login '${{ inputs.steamUser }}' '${{ inputs.steamPassword }}' '${{ steps.steam-totp-playtest.outputs.code }}' \
+run_app_build ${preview} ${build_file:a} \
+quit
print '::endgroup'
popd
- name: Upload Steam build logs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: steam-build-logs
path: ${{ github.workspace }}/steam/build/*.log

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,149 @@
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-Group {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
[string[]] $Message
)
Process {
if ( $Env:CI -ne $null ) {
if ( $script:LogGroup ) {
Write-Output '::endgroup::'
$script:LogGroup = $false
}
if ( $Message.count -ge 1 ) {
Write-Output "::group::$($Message -join ' ')"
$script:LogGroup = $true
}
} else {
if ( $Message.count -ge 1 ) {
Log-Information $Message
}
}
}
}
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

View File

@ -0,0 +1,90 @@
name: Run PVS-Studio Analysis
inputs:
pvsUsername:
description: PVS-Studio License Username
required: true
pvsKey:
description: PVS-Studio License Key
required: true
target:
description: Build Target
required: true
config:
description: Build Configuration
required: true
runs:
using: composite
steps:
- name: Setup PVS-Studio
shell: pwsh
run: |
choco install pvs-studio --version=7.30.81185.980 -y --no-progress
- name: Activate PVS-Studio
shell: pwsh
run: |
. ${env:GITHUB_ACTION_PATH}\Invoke-External.ps1
Invoke-External "C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe" credentials -u ${{ inputs.pvsUsername }} -n ${{ inputs.pvsKey }}
- name: Run PVS-Studio Analysis
shell: pwsh
run: |
[flags()] Enum PVSErrorCodes {
Success = 0
AnalyzerCrash = 1
GenericError = 2
InvalidCommandLine = 4
FileNotFound = 8
ConfigurationNotFound = 16
InvalidProject = 32
InvalidExtension = 64
LicenseInvalid = 128
CodeErrorsFound = 256
SuppressionFailed = 512
LicenseExpiringSoon = 1024
}
$pvsParams = @(
"--progress"
"--disableLicenseExpirationCheck"
"-p", "${{ inputs.target }}"
"-c", "${{ inputs.config }}"
"-t", "${{ github.workspace }}\build_x64\obs-studio.sln"
"-o", "${{ github.workspace }}\analysis.plog"
"-C", "${env:GITHUB_ACTION_PATH}\obs.pvsconfig"
)
& "C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe" @pvsParams
# Success, LicenseExpiringSoon, and CodeErrorsFound are fine as error codes, we only care if it is anything but those
$pvs_result = $LASTEXITCODE -band (-bnot ([PVSErrorCodes]::CodeErrorsFound -bor [PVSErrorCodes]::LicenseExpiringSoon))
if ($pvs_result -ne 0) {
Write-Output "PVS-Studio Errors: $([PVSErrorCodes]$pvs_result)"
}
exit $pvs_result
- name: Convert Analysis to SARIF
shell: pwsh
run: |
. ${env:GITHUB_ACTION_PATH}\Invoke-External.ps1
$conversionParams = @(
"-a", "GA:1,2",
"-d", "V1042,Renew"
"-t", "Sarif"
"${{ github.workspace }}\analysis.plog"
)
Invoke-External "C:\Program Files (x86)\PVS-Studio\PlogConverter.exe" @conversionParams
- name: Upload PVS-Studio Logs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: 'pvs-analysis-log'
path: |
${{ github.workspace }}/analysis.plog
${{ github.workspace }}/analysis.plog.sarif
- name: Upload PVS-Studio Report
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
sarif_file: "${{ github.workspace }}/analysis.plog.sarif"
category: 'PVS-Studio (Windows)'

View File

@ -0,0 +1,10 @@
//V_EXCLUDE_PATH */.deps/*
//V_EXCLUDE_PATH */blake2/*
//V_EXCLUDE_PATH */json11/*
//V_EXCLUDE_PATH */simde/*
//V_EXCLUDE_PATH */w32-pthreads/*
//V_EXCLUDE_PATH */plugins/obs-browser/*
//V_EXCLUDE_PATH */plugins/obs-outputs/ftl-sdk/*
//V_EXCLUDE_PATH */plugins/obs-websocket/*
//V_EXCLUDE_PATH */plugins/win-dshow/libdshowcapture/*
//V_EXCLUDE_PATH *_autogen/*

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,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,149 @@
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-Group {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
[string[]] $Message
)
Process {
if ( $Env:CI -ne $null ) {
if ( $script:LogGroup ) {
Write-Output '::endgroup::'
$script:LogGroup = $false
}
if ( $Message.count -ge 1 ) {
Write-Output "::group::$($Message -join ' ')"
$script:LogGroup = $true
}
} else {
if ( $Message.count -ge 1 ) {
Log-Information $Message
}
}
}
}
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

View File

@ -0,0 +1,126 @@
name: Run bouf Patch Generation
description: Generates OBS updater manifest and patches
inputs:
gcsAccessKeyId:
description: GCS S3 Access Key ID
required: true
gcsAccessKeySecret:
description: GCS S3 Access Key Secret
required: true
workflowSecret:
description: GitHub API token to use for API calls
required: true
tagName:
description: GitHub Release tag
required: true
channel:
description: Update channel
required: false
default: 'stable'
architecture:
description: OBS build architecture
required: false
default: 'x64'
runs:
using: composite
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: "repo"
fetch-depth: 0
ref: ${{ inputs.tagName }}
- name: Download Release Artifact
shell: pwsh
env:
GH_TOKEN: ${{ inputs.workflowSecret }}
run: |
# Download OBS release
. ${env:GITHUB_ACTION_PATH}\Invoke-External.ps1
Invoke-External gh release download "${{ inputs.tagName }}" -p "OBS-Studio-${{ inputs.tagName }}-Windows-${{ inputs.architecture }}.zip"
Invoke-External gh attestation verify "OBS-Studio-${{ inputs.tagName }}-Windows-${{ inputs.architecture }}.zip" --owner obsproject
Expand-Archive -Path "OBS-Studio-${{ inputs.tagName }}-Windows-${{ inputs.architecture }}.zip" -DestinationPath "${{ github.workspace }}/build"
- name: Setup bouf
shell: pwsh
env:
BOUF_TAG: 'v0.6.5'
BOUF_HASH: '2e16d5116415579b5dd8cd68b3fe6edaa7513938427567121942f592292605d5'
BOUF_NSIS_HASH: 'e323043627cfeebd237d306afc001c8c36a3c37f6ecef2b349a19a3209ae3450'
GH_TOKEN: ${{ github.token }}
run: |
# Download bouf release
. ${env:GITHUB_ACTION_PATH}\Ensure-Location.ps1
. ${env:GITHUB_ACTION_PATH}\Invoke-External.ps1
Ensure-Location bouf
$windows_zip = "bouf-windows-${env:BOUF_TAG}.zip"
$nsis_zip = "bouf-nsis-${env:BOUF_TAG}.zip"
Invoke-External gh release download "${env:BOUF_TAG}" -R "obsproject/bouf" -p $windows_zip -p $nsis_zip
if ((Get-FileHash $windows_zip -Algorithm SHA256).Hash -ne "${env:BOUF_HASH}") {
throw "bouf hash does not match."
}
if ((Get-FileHash $nsis_zip -Algorithm SHA256).Hash -ne "${env:BOUF_NSIS_HASH}") {
throw "NSIS package hash does not match."
}
Expand-Archive -Path $windows_zip -DestinationPath bin
Expand-Archive -Path $nsis_zip -DestinationPath nsis
- name: Install NSIS
shell: pwsh
run: |
# Install NSIS
winget install --silent --accept-package-agreements --accept-source-agreements --disable-interactivity -e --id NSIS.NSIS
- name: Install rclone and pandoc
shell: pwsh
run: |
choco install rclone pandoc -y --no-progress
- name: Download Previous Builds
shell: pwsh
env:
RCLONE_TRANSFERS: '100'
RCLONE_FAST_LIST: 'true'
RCLONE_EXCLUDE: '"{pdbs/**,**/${{ inputs.tagName }}/**}"'
RCLONE_S3_PROVIDER: 'GCS'
RCLONE_S3_ACCESS_KEY_ID: '${{ inputs.gcsAccessKeyId }}'
RCLONE_S3_SECRET_ACCESS_KEY: '${{ inputs.gcsAccessKeySecret }}'
RCLONE_S3_ENDPOINT: 'https://storage.googleapis.com'
run: |
rclone -q copy ":s3:obs-builds/${{ inputs.architecture }}" "${{ github.workspace }}/old_builds"
- name: Prepare Release Notes
shell: pwsh
run: |
# Release notes are just the tag body on Windows
Set-Location repo
git tag -l --format='%(contents:subject)' ${{ inputs.tagName }} > "${{ github.workspace }}/notes.rst"
Write-Output "###################################################" >> "${{ github.workspace }}/notes.rst"
Write-Output "" >> "${{ github.workspace }}/notes.rst"
git tag -l --format='%(contents:body)' ${{ inputs.tagName }} >> "${{ github.workspace }}/notes.rst"
- name: Run bouf
shell: pwsh
run: |
. ${env:GITHUB_ACTION_PATH}\Invoke-External.ps1
$boufArgs = @(
"--config", "${env:GITHUB_ACTION_PATH}/config.toml",
"--version", "${{ inputs.tagName }}"
"--branch", "${{ inputs.channel }}"
"--notes-file", "${{ github.workspace }}/notes.rst"
"-i", "${{ github.workspace }}/build"
"-p", "${{ github.workspace }}/old_builds"
"-o", "${{ github.workspace }}/output"
"--updater-data-only"
)
Invoke-External "${{ github.workspace }}\bouf\bin\bouf.exe" @boufArgs
- name: Upload Outputs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: windows-updater-files
compression-level: 0
path: ${{ github.workspace }}/output

View File

@ -0,0 +1,31 @@
[general]
log_level = "trace"
[env]
# On CI these should be in %PATH%
sevenzip_path = "7z"
makensis_path = "C:/Program Files (x86)/NSIS/makensis.exe"
pandoc_path = "pandoc"
pdbcopy_path = "C:/Program Files (x86)/Windows Kits/10/Debuggers/x64/pdbcopy.exe"
[prepare.codesign]
skip_sign = true
[generate]
patch_type = "zstd"
compress_files = true
# Prevent some legacy plugins from being marked as deleted
exclude_from_removal = [
"enc-amf",
"win-mf",
]
[package]
[package.installer]
skip = true
[package.updater]
vc_redist_path = "bouf/nsis/VC_redist.x64.exe"
skip_sign = true

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,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,149 @@
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-Group {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
[string[]] $Message
)
Process {
if ( $Env:CI -ne $null ) {
if ( $script:LogGroup ) {
Write-Output '::endgroup::'
$script:LogGroup = $false
}
if ( $Message.count -ge 1 ) {
Write-Output "::group::$($Message -join ' ')"
$script:LogGroup = $true
}
} else {
if ( $Message.count -ge 1 ) {
Log-Information $Message
}
}
}
}
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

View File

@ -0,0 +1,149 @@
name: Run bouf Packaging
description: Generates signed OBS install files
inputs:
gcpWorkloadIdentityProvider:
description: GCP Identity Provider Pool ID
required: true
gcpServiceAccountName:
description: Google service account name
required: true
version:
description: Version string (e.g., 30.0.0-rc1)
required: true
channel:
description: Update channel
required: false
default: 'stable'
architecture:
description: OBS build architecture
required: false
default: 'x64'
runs:
using: composite
steps:
- name: Extract Artifact
shell: pwsh
run: |
Expand-Archive -Path build\*.zip -DestinationPath build
Remove-Item build\*.zip
- name: Setup bouf
shell: pwsh
env:
BOUF_TAG: 'v0.6.5'
BOUF_HASH: '2e16d5116415579b5dd8cd68b3fe6edaa7513938427567121942f592292605d5'
BOUF_NSIS_HASH: 'e323043627cfeebd237d306afc001c8c36a3c37f6ecef2b349a19a3209ae3450'
GH_TOKEN: ${{ github.token }}
run: |
# Download bouf release
. ${env:GITHUB_ACTION_PATH}\Ensure-Location.ps1
. ${env:GITHUB_ACTION_PATH}\Invoke-External.ps1
Ensure-Location bouf
$windows_zip = "bouf-windows-${env:BOUF_TAG}.zip"
$nsis_zip = "bouf-nsis-${env:BOUF_TAG}.zip"
Invoke-External gh release download "${env:BOUF_TAG}" -R "obsproject/bouf" -p $windows_zip -p $nsis_zip
if ((Get-FileHash $windows_zip -Algorithm SHA256).Hash -ne "${env:BOUF_HASH}") {
throw "bouf hash does not match."
}
if ((Get-FileHash $nsis_zip -Algorithm SHA256).Hash -ne "${env:BOUF_NSIS_HASH}") {
throw "NSIS package hash does not match."
}
Expand-Archive -Path $windows_zip -DestinationPath bin
Expand-Archive -Path $nsis_zip -DestinationPath nsis
- name: Install NSIS
shell: pwsh
run: |
# Install NSIS
winget install --silent --accept-package-agreements --accept-source-agreements --disable-interactivity -e --id NSIS.NSIS
- name: Setup Google CNG Provider
shell: pwsh
env:
CNG_TAG: 'cng-v1.1'
GH_TOKEN: ${{ github.token }}
run: |
# Download Google CNG provider release from github
. ${env:GITHUB_ACTION_PATH}\Ensure-Location.ps1
. ${env:GITHUB_ACTION_PATH}\Invoke-External.ps1
Ensure-Location gcng
Invoke-External gh release download "${env:CNG_TAG}" -R "GoogleCloudPlatform/kms-integrations" -p "*amd64.zip"
Expand-Archive -Path *.zip
$sigPath = Get-ChildItem *.sig -Recurse
$msiPath = Get-ChildItem *.msi -Recurse
# Verify digital signature against Google's public key
Invoke-External openssl dgst -sha384 -verify "${env:GITHUB_ACTION_PATH}/cng-release-signing-key.pem" -signature $sigPath $msiPath
# Finally, install the CNG provider
Invoke-External msiexec /i $msiPath /qn /norestart
- name: Install rclone
shell: pwsh
run: |
choco install rclone --version=1.69.3 -y --no-progress
- name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093' # v3.0.0
with:
workload_identity_provider: ${{ inputs.gcpWorkloadIdentityProvider }}
service_account: ${{ inputs.gcpServiceAccountName }}
- name: Download Previous Build
shell: pwsh
env:
RCLONE_GCS_ENV_AUTH: 'true'
run: |
. ${env:GITHUB_ACTION_PATH}\Ensure-Location.ps1
Ensure-Location "${{ github.workspace }}/old_builds"
rclone copy --transfers 100 ":gcs:obs-latest/${{ inputs.architecture }}/${{ inputs.channel }}" .
- name: Sign Game Capture with RSA cert
shell: pwsh
run: |
. ${env:GITHUB_ACTION_PATH}\Invoke-External.ps1
$SignToolExe = "C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe"
$signArgs = @(
"sign"
"/fd", "sha256"
"/t", "http://timestamp.digicert.com"
"/f", "repo/.github/actions/windows-signing/prod-gc.crt"
"/csp", "Google Cloud KMS Provider"
"/kc", "projects/ci-signing/locations/global/keyRings/production/cryptoKeys/game-capture-release-sign-hsm/cryptoKeyVersions/1"
"${{ github.workspace }}/build/data/obs-plugins/win-capture/*.dll"
)
Invoke-External $SignToolExe @signArgs
- name: Run bouf
shell: pwsh
run: |
. ${env:GITHUB_ACTION_PATH}\Invoke-External.ps1
$boufArgs = @(
"--config", "${env:GITHUB_ACTION_PATH}/config_${{ inputs.architecture }}.toml"
"--version", "${{ inputs.version }}"
"--branch", "${{ inputs.channel }}"
"-i", "${{ github.workspace }}/build"
"-p", "${{ github.workspace }}/old_builds"
"-o", "${{ github.workspace }}/output"
"--packaging-only"
)
Invoke-External "${{ github.workspace }}\bouf\bin\bouf.exe" @boufArgs
- name: Sync Latest Build
shell: pwsh
env:
RCLONE_INCLUDE: '**/${{ inputs.version }}/**'
RCLONE_GCS_ENV_AUTH: 'true'
RCLONE_GCS_BUCKET_POLICY_ONLY: 'true'
run: |
rclone sync --delete-excluded --transfers 100 "${{ github.workspace }}/old_builds" ":gcs:obs-latest/${{ inputs.architecture }}/${{ inputs.channel }}"
- name: Upload Build to Archive
shell: pwsh
env:
RCLONE_GCS_ENV_AUTH: 'true'
RCLONE_GCS_BUCKET_POLICY_ONLY: 'true'
run: |
rclone copy --transfers 100 "${{ github.workspace }}/old_builds" ":gcs:obs-builds/${{ inputs.architecture }}"

View File

@ -0,0 +1,5 @@
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEtfLbXkHUVc9oUPTNyaEK3hIwmuGRoTtd
6zDhwqjJuYaMwNd1aaFQLMawTwZgR0Xn27ymVWtqJHBe0FU9BPIQ+SFmKw+9jSwu
/FuqbJnLmTnWMJ1jRCtyHNZawvv2wbiB
-----END PUBLIC KEY-----

View File

@ -0,0 +1,47 @@
[general]
log_level = "trace"
[env]
# On CI these should be in %PATH%
sevenzip_path = "7z"
makensis_path = "C:/Program Files (x86)/NSIS/makensis.exe"
pandoc_path = "pandoc"
pdbcopy_path = "C:/Program Files (x86)/Windows Kits/10/Debuggers/x64/pdbcopy.exe"
## Preparation steps
[prepare]
[prepare.copy]
never_copy = [
"bin/32bit",
"obs-plugins/32bit",
".keepme",
]
[prepare.codesign]
sign_cert_file = "repo/.github/actions/windows-signing/prod.crt"
sign_kms_key_id = "projects/ci-signing/locations/global/keyRings/production/cryptoKeys/release-sign-hsm/cryptoKeyVersions/1"
sign_digest = "sha384"
sign_ts_serv = "http://timestamp.digicert.com"
sign_ts_algo = "sha256"
sign_exts = ['exe', 'dll', 'pyd']
sign_append = true
[prepare.strip_pdbs]
# PDBs to not strip
exclude = [
"obs-frontend-api.pdb",
"obs64.pdb",
"obs.pdb",
]
[package]
[package.installer]
skip = true
[package.zip]
name = "OBS-Studio-{version}-arm64.zip"
pdb_name = "OBS-Studio-{version}-arm64-pdbs.zip"
[package.updater]
skip_sign = true

View File

@ -0,0 +1,47 @@
[general]
log_level = "trace"
[env]
# On CI these should be in %PATH%
sevenzip_path = "7z"
makensis_path = "C:/Program Files (x86)/NSIS/makensis.exe"
pandoc_path = "pandoc"
pdbcopy_path = "C:/Program Files (x86)/Windows Kits/10/Debuggers/x64/pdbcopy.exe"
## Preparation steps
[prepare]
[prepare.copy]
never_copy = [
"bin/32bit",
"obs-plugins/32bit",
".keepme",
]
[prepare.codesign]
sign_cert_file = "repo/.github/actions/windows-signing/prod.crt"
sign_kms_key_id = "projects/ci-signing/locations/global/keyRings/production/cryptoKeys/release-sign-hsm/cryptoKeyVersions/1"
sign_digest = "sha384"
sign_ts_serv = "http://timestamp.digicert.com"
sign_ts_algo = "sha256"
sign_exts = ['exe', 'dll', 'pyd']
sign_append = true
[prepare.strip_pdbs]
# PDBs to not strip
exclude = [
"obs-frontend-api.pdb",
"obs64.pdb",
"obs.pdb",
]
[package]
[package.installer]
nsis_script = "bouf/nsis/mp-installer.nsi"
[package.zip]
name = "OBS-Studio-{version}-x64.zip"
pdb_name = "OBS-Studio-{version}-pdbs.zip"
[package.updater]
skip_sign = true

View File

@ -0,0 +1,42 @@
-----BEGIN CERTIFICATE-----
MIIHYDCCBUigAwIBAgIQCTRoMeK9NItAQamXbU8PnjANBgkqhkiG9w0BAQsFADBp
MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMT
OERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0
IDIwMjEgQ0ExMB4XDTI1MTExNzAwMDAwMFoXDTI4MDExNTIzNTk1OVowaDELMAkG
A1UEBhMCVVMxEDAOBgNVBAgTB1d5b21pbmcxETAPBgNVBAcTCFNoZXJpZGFuMRkw
FwYDVQQKExBPQlMgUHJvamVjdCwgTExDMRkwFwYDVQQDExBPQlMgUHJvamVjdCwg
TExDMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA07e66QJeFjyk8p5l
1/hOBt5qXf8paJIFoBsdy38qnkC6ZTJzrmSfERilRBM7UQ7Pzo9aE/On7aUrghdW
ZfG/U/3s4KKYZMh+mQscHdx37Y4sUC0Yk/3s+1H3jAz5tEx9FlUgO30MKjSTr3Lc
HjqoibokGrZOzqSF2pLqTmSX92/P7E9ii2EnZnTSDWHHLtVmS0YkE6TKQ5v2VHYP
ynRVWuOl2wJFNctCYbcZAmBeVFne4k6w443Zvkz70m4lgtaJB24r2y2ay+vyQx2Q
gEg3RgcW+3/zh/sqjCQ6RhUjFvdBHP9nPrhCw72P/2J04JrpMnTlHbwUp1ULyH9v
rOYDu+8gk2sFgwjgKYGrjuehtwG8IokCppWPxUUyDTklFXbjDVlLQizmoPjwfUKy
K6kJd6w6WR3jUdRZYIXuHPzzIQE3G2aB68tSyYANuXjQAOXtVKkFlMiI/KGATIKb
FCnhFriqFOlG1vxeKUgqMNQqcaz52V8ZGEtVAOMZVP0FzZIDqrFwvDTQwsRVsRUU
c6ACUGZVL5X5nn90XTYIf4oZGFIs7U/P+LmH7Hngb3ZnrvwhurSreaELR554ncOl
fLJGpiJlTShkvubXycmYIiM+XLVkdziZlRFlMef5hp02fuT+825ivuWzaNTB0min
hMatLBKIwxjO5Xlk6CztRQD6ezMCAwEAAaOCAgMwggH/MB8GA1UdIwQYMBaAFGg3
4Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBSNjnGJqRrmOQnj5YyA9Ax8ZpJ/
ejA+BgNVHSAENzA1MDMGBmeBDAEEATApMCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3
LmRpZ2ljZXJ0LmNvbS9DUFMwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsG
AQUFBwMDMIG1BgNVHR8Ega0wgaowU6BRoE+GTWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0
LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIw
MjFDQTEuY3JsMFOgUaBPhk1odHRwOi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaUNl
cnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNybDCB
lAYIKwYBBQUHAQEEgYcwgYQwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj
ZXJ0LmNvbTBcBggrBgEFBQcwAoZQaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t
L0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNB
MS5jcnQwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAgEAKhWQvdUAI3TLwy0j
DBZflHdzq2qnsF7v7OCQEiG46CA1/37ZJrBx6B5u23Z4dS7Ruji679WAnhq5oxkC
uRVwmjtcDmZ8m1iDmbN0yZzfyhzWYUDQJfOQRtRVJC/3MKy6uPw9IZsInOZDWEeg
rVoKLnkrxSbFFRoKtzOrkFqO+bdo+atEUbYsPFAMXzvXTlEvuYqB5iWWbtA2/wN7
dFPkB1T9PBp803PU10AeKxwI4q+Ih+Iul3hiPflvFtoY4SX8JBioEWBugcmiL6w9
8A88zGnONFcuA2iVFjvJKon6g87mQMulqmZGgBjNmXipDQlsC3rUkdZHJrsKq6mI
Pkx5EFfIKJ/ibSQ5f6eKfPboh6hF7JutdoOEZ8Zo2A5M/1WV/kO18IJsKGDPEWBa
Y+kvSzdU1j9p7IGU6QRLrBTHO5yUyIoEVAXIPs4g6bRawFHVcO8zebAhg+CmDWsG
JT7BDQeiVWuFHs9w6YlQoPQsCl7SL0v8d2JeXajchK65owCyV6dNctBzIIU0/5tW
eHGB+jd9Sd6b8z8lgdTBQRBMOxBw5+GGieR6VklPqnLo6nZiX1PVPauTIrp0XP0E
Z/n6TCQpUc+EqD7dFAjxRQh0HV3Zx2Xc8QCtLc2cI+ShQrgGXyqo0GlhalNDZkj8
WbVFjeU1iU9tPlee5BDXberDjFg=
-----END CERTIFICATE-----

View File

@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEIzCCA6mgAwIBAgIQA/1HO4x/Q51pGXj/sSt/ZzAKBggqhkjOPQQDAzBTMQsw
CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xKzApBgNVBAMTIkRp
Z2lDZXJ0IEc1IENTIEVDQyBTSEEzODQgMjAyMSBDQTEwHhcNMjYwMzA1MDAwMDAw
WhcNMjcwMTI3MjM1OTU5WjCBzzETMBEGCysGAQQBgjc8AgEDEwJVUzEYMBYGCysG
AQQBgjc8AgECEwdXeW9taW5nMR0wGwYDVQQPDBRQcml2YXRlIE9yZ2FuaXphdGlv
bjEXMBUGA1UEBRMOMjAyMy0wMDEyNzIyNTIxCzAJBgNVBAYTAlVTMRAwDgYDVQQI
EwdXeW9taW5nMREwDwYDVQQHEwhTaGVyaWRhbjEZMBcGA1UEChMQT0JTIFByb2pl
Y3QsIExMQzEZMBcGA1UEAxMQT0JTIFByb2plY3QsIExMQzB2MBAGByqGSM49AgEG
BSuBBAAiA2IABKkywk1iHNOnpOU7QEAt5fddCH57pHHdxJxrgbj06UONaLHDY3AR
gjqYbVBHYIr59lW59l0Xg/ruWzaX+YHWhJrzriaCI/zTf8lambBEDJBfIkMDQD8k
JDxfza4DulU0eaOCAcMwggG/MB8GA1UdIwQYMBaAFNcdx/rG+oJ0IEVxNBIvVdWY
QZJeMB0GA1UdDgQWBBTz68Ag8TYugImX7RVRd3qem1tY2zA9BgNVHSAENjA0MDIG
BWeBDAEDMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQ
UzAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgY0GA1UdHwSB
hTCBgjA/oD2gO4Y5aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0RzVD
U0VDQ1NIQTM4NDIwMjFDQTEuY3JsMD+gPaA7hjlodHRwOi8vY3JsNC5kaWdpY2Vy
dC5jb20vRGlnaUNlcnRHNUNTRUNDU0hBMzg0MjAyMUNBMS5jcmwwfgYIKwYBBQUH
AQEEcjBwMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wSAYI
KwYBBQUHMAKGPGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEc1
Q1NFQ0NTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMAoGCCqGSM49BAMDA2gA
MGUCMDuU78m1lTDkCHRpMd1nOvvuRCTSglIo+pN9T0G1ziPi+MyeyVmERc2lqXlW
TANo+wIxAL+y4Ttsw4Vs9qoFArNpA0EIIz2tKdKgRGNjcPNSZq6ZQkiyVFN4xsjc
FCONL0pZJw==
-----END CERTIFICATE-----

View File

@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBpDCCAUmgAwIBAgIUXeKu2+AXK2yR8WyuTTRg8+t6p/kwCgYIKoZIzj0EAwIw
JzElMCMGA1UEAwwcV2l6YXJkcyBvZiBPQlMgTExDIChURVNUSU5HKTAeFw0yMzEx
MDQwMDA3NDBaFw0zMzExMDEwMDA3NDBaMCcxJTAjBgNVBAMMHFdpemFyZHMgb2Yg
T0JTIExMQyAoVEVTVElORykwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARVAdL6
pkfHYuO8rtndlAjpbz9bMBIVN8elrg4cDJkReAqsjH78ulhKvkor6lfF7rbRzIKA
u026v1aDE79Z+bz+o1MwUTAdBgNVHQ4EFgQUsFQqmZEEapghq7ZPqpejjFeu8Dsw
HwYDVR0jBBgwFoAUsFQqmZEEapghq7ZPqpejjFeu8DswDwYDVR0TAQH/BAUwAwEB
/zAKBggqhkjOPQQDAgNJADBGAiEAmNSMQqZu3U9eMP+PbmssCigkbFKG8mGLNLFN
J+KfcdkCIQCVOjg6cqk4zFe7H8EKhB6CwOAIOQrgjGWLihtO2lovCw==
-----END CERTIFICATE-----

7
obs-studio/.github/scripts/.Aptfile vendored Normal file
View File

@ -0,0 +1,7 @@
package 'ccache'
package 'cmake', bin: '/usr/bin/cmake'
package 'curl'
package 'git'
package 'jq'
package 'ninja-build', bin: 'ninja'
package 'pkg-config'

4
obs-studio/.github/scripts/.Brewfile vendored Normal file
View File

@ -0,0 +1,4 @@
brew "cmake"
brew "git"
brew "jq"
brew "xcbeautify"

View File

@ -0,0 +1,2 @@
package '7zip.7zip', path: '7-zip', bin: '7z'
package 'cmake', path: 'Cmake\bin', bin: 'cmake'

239
obs-studio/.github/scripts/.build.zsh vendored Normal file
View File

@ -0,0 +1,239 @@
#!/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
if (( ! ${+CI} )) {
print -u2 -PR "%F{1} ✖︎ ${ZSH_ARGZERO:t:r} requires CI environment.%f"
exit 1
}
autoload -Uz is-at-least && if ! is-at-least 5.9; then
print -u2 -PR "%F{1}${funcstack[1]##*/}:%f Running on Zsh version %B${ZSH_VERSION}%b, but Zsh %B5.9%b is the minimum supported version. Upgrade Zsh to fix this issue."
exit 1
fi
TRAPZERR() {
print -u2 -PR "::error::%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 project_root=${SCRIPT_HOME:A:h:h}
fpath=(${SCRIPT_HOME}/utils.zsh ${fpath})
autoload -Uz log_group log_error log_output check_${host_os}
local -i debug=0
local target
local -r -a _valid_targets=(
macos-x86_64
macos-arm64
ubuntu-x86_64
)
local config='RelWithDebInfo'
local -r -a _valid_configs=(Debug RelWithDebInfo Release MinSizeRel)
local -i codesign=0
local -i analyze=0
local -a args
while (( # )) {
case ${1} {
-t|--target|-c|--config)
if (( # == 1 )) || [[ ${2:0:1} == '-' ]] {
log_error "Missing value for option %B${1}%b"
exit 2
}
;;
}
case ${1} {
--) shift; args+=($@); break ;;
-a|--analyze) analyze=1; shift ;;
-t|--target)
if (( ! ${_valid_targets[(Ie)${2}]} )) {
log_error "Invalid value %B${2}%b for option %B${1}%b"
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"
exit 2
}
config=${2}
shift 2
;;
-s|--codesign) codesign=1; shift ;;
--debug) debug=1; shift ;;
*) log_error "Unknown option: %B${1}%b"; log_output ${_usage}; exit 2 ;;
}
}
: "${target:="${host_os}-${CPUTYPE}"}"
set -- ${(@)args}
check_${host_os}
if [[ ${host_os} == ubuntu ]] {
autoload -Uz setup_ubuntu setup_ccache
setup_ccache
setup_ubuntu
}
local product_name='obs-studio'
pushd ${project_root}
local -a cmake_args=()
local -a cmake_build_args=(--build)
local -a cmake_install_args=(--install)
if (( debug )) cmake_args+=(--debug-output)
case ${target} {
macos-*)
cmake_args+=(
--preset 'macos-ci'
-DCMAKE_OSX_ARCHITECTURES:STRING=${target##*-}
)
if (( debug )) {
cmake_args+=(CMAKE_XCODE_ATTRIBUTE_COMPILATION_CACHE_ENABLE_DIAGNOSTIC_REMARKS:STRING=YES)
}
typeset -gx NSUnbufferedIO=YES
typeset -gx CODESIGN_IDENT="${CODESIGN_IDENT:--}"
if (( codesign )) && [[ -z ${CODESIGN_TEAM} ]] {
typeset -gx CODESIGN_TEAM="$(print "${CODESIGN_IDENT}" | /usr/bin/sed -En 's/.+\((.+)\)/\1/p')"
}
log_group "Configuring ${product_name}..."
cmake -S ${project_root} ${cmake_args}
log_group "Building ${product_name}..."
run_xcodebuild() {
if (( debug )) {
xcodebuild ${@}
} else {
if [[ ${GITHUB_EVENT_NAME} == push ]] {
xcodebuild ${@} 2>&1 | xcbeautify --renderer terminal
} else {
xcodebuild ${@} 2>&1 | xcbeautify --renderer github-actions
}
}
}
local -a build_args=(
ONLY_ACTIVE_ARCH=NO
-project obs-studio.xcodeproj
-target obs-studio
-destination "generic/platform=macOS,name=Any Mac"
-configuration ${config}
-parallelizeTargets
-hideShellScriptEnvironment
build
)
local -a archive_args=(
ONLY_ACTIVE_ARCH=NO
-project obs-studio.xcodeproj
-scheme obs-studio
-destination "generic/platform=macOS,name=Any Mac"
-archivePath obs-studio.xcarchive
-parallelizeTargets
-hideShellScriptEnvironment
archive
)
local -a export_args=(
-exportArchive
-archivePath obs-studio.xcarchive
-exportOptionsPlist exportOptions.plist
-exportPath ${project_root}/build_macos
)
local -a analyze_args=(
CLANG_ANALYZER_OUTPUT=sarif
CLANG_ANALYZER_OUTPUT_DIR=${project_root}/analytics
-project obs-studio.xcodeproj
-target obs-studio
-destination "generic/platform=macOS,name=Any Mac"
-configuration ${config}
-parallelizeTargets
-hideShellScriptEnvironment
analyze
)
pushd build_macos
if (( analyze )) {
run_xcodebuild ${analyze_args}
} else {
if [[ ${GITHUB_EVENT_NAME} == push && ${GITHUB_REF_NAME} =~ [0-9]+.[0-9]+.[0-9]+(-(rc|beta).+)? ]] {
run_xcodebuild ${archive_args}
run_xcodebuild ${export_args}
} else {
run_xcodebuild ${build_args}
rm -rf OBS.app
mkdir OBS.app
ditto frontend/${config}/OBS.app OBS.app
}
}
popd
;;
ubuntu-*)
local cmake_bin='/usr/bin/cmake'
cmake_args+=(
--preset ubuntu-ci
-DENABLE_BROWSER:BOOL=ON
-DCEF_ROOT_DIR:PATH="${project_root}/.deps/cef_binary_${CEF_VERSION}_${target//ubuntu-/linux_}"
)
cmake_build_args+=(build_${target%%-*} --config ${config} --parallel)
cmake_install_args+=(build_${target%%-*} --prefix ${project_root}/build_${target%%-*}/install/${config})
export CLICOLOR_FORCE=1
log_group "Configuring ${product_name}..."
${cmake_bin} -S ${project_root} ${cmake_args}
log_group "Building ${product_name}..."
if (( debug )) cmake_build_args+=(--verbose)
${cmake_bin} ${cmake_build_args}
log_group "Installing ${product_name}..."
if (( debug )) cmake_install_args+=(--verbose)
${cmake_bin} ${cmake_install_args}
;;
}
popd
log_group
}
build ${@}

251
obs-studio/.github/scripts/.package.zsh vendored Normal file
View File

@ -0,0 +1,251 @@
#!/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
if (( ! ${+CI} )) {
print -u2 -PR "%F{1} ✖︎ ${ZSH_ARGZERO:t:r} requires CI environment%f"
exit 1
}
autoload -Uz is-at-least && if ! is-at-least 5.9; then
print -u2 -PR "%F{1}${funcstack[1]##*/}:%f Running on Zsh version %B${ZSH_VERSION}%b, but Zsh %B5.9%b is the minimum supported version. Upgrade Zsh to fix this issue."
exit 1
fi
TRAPZERR() {
print -u2 -PR "::error::%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 project_root=${SCRIPT_HOME:A:h:h}
fpath=(${SCRIPT_HOME}/utils.zsh ${fpath})
autoload -Uz log_error log_output log_group check_${host_os}
local -i debug=0
local target
local -r -a _valid_targets=(
macos-x86_64
macos-arm64
ubuntu-x86_64
)
local config='RelWithDebInfo'
local -r -a _valid_configs=(Debug RelWithDebInfo Release MinSizeRel)
local -i codesign=0
local -i notarize=0
local -i package=0
local -i skip_deps=0
local -a args
while (( # )) {
case ${1} {
-t|--target|-c|--config)
if (( # == 1 )) || [[ ${2:0:1} == '-' ]] {
log_error "Missing value for option %B${1}%b"
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"
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"
exit 2
}
config=${2}
shift 2
;;
-s|--codesign) codesign=1; shift ;;
-n|--notarize) notarize=1; shift ;;
-p|--package) typeset -g package=1; shift ;;
--debug) debug=0; shift ;;
*) log_error "Unknown option: %B${1}%b"; log_output ${_usage}; exit 2 ;;
}
}
: "${target:="${host_os}-${CPUTYPE}"}"
set -- ${(@)args}
check_${host_os}
local product_name='obs-studio'
local commit_version='0.0.0'
local commit_distance='0'
local commit_hash
if [[ -d ${project_root}/.git ]] {
local git_description="$(git describe --tags --long)"
commit_version="${${git_description%-*}%-*}"
commit_hash="${git_description##*-g}"
commit_distance="${${git_description%-*}##*-}"
}
local output_name
if (( commit_distance > 0 )) {
output_name="obs-studio-${commit_version}-${commit_hash}"
} else {
output_name="obs-studio-${commit_version}"
}
if [[ ${host_os} == macos ]] {
if [[ ! -d build_macos/OBS.app ]] {
log_error 'No application bundle found. Run the build script to create a valid application bundle.'
return 0
}
local -A arch_names=(x86_64 Intel arm64 Apple)
output_name="${output_name}-macos-${(L)arch_names[${target##*-}]}"
local volume_name
if (( commit_distance > 0 )) {
volume_name="OBS Studio ${commit_version}-${commit_hash} (${arch_names[${target##*-}]})"
} else {
volume_name="OBS Studio ${commit_version} (${arch_names[${target##*-}]})"
}
if (( package )) {
pushd build_macos
mkdir -p obs-studio/.background
cp ${project_root}/cmake/macos/resources/background.tiff obs-studio/.background/
cp ${project_root}/cmake/macos/resources/AppIcon.icns obs-studio/.VolumeIcon.icns
ln -s /Applications obs-studio/Applications
mkdir -p obs-studio/OBS.app
ditto OBS.app obs-studio/OBS.app
local -i _status=0
autoload -Uz create_diskimage
create_diskimage obs-studio ${volume_name} ${output_name} || _status=1
rm -r obs-studio
if (( _status )) {
log_error "Disk image creation failed."
return 2
}
typeset -gx CODESIGN_IDENT="${CODESIGN_IDENT:--}"
typeset -gx CODESIGN_TEAM="$(print "${CODESIGN_IDENT}" | /usr/bin/sed -En 's/.+\((.+)\)/\1/p')"
codesign --sign "${CODESIGN_IDENT}" ${output_name}.dmg
if (( codesign && notarize )) {
if ! [[ ${CODESIGN_IDENT} != '-' && ${CODESIGN_TEAM} && ${CODESIGN_IDENT_USER} && ${CODESIGN_IDENT_PASS} ]] {
log_error "Notarization requires Apple ID and application password."
return 2
}
xcrun notarytool store-credentials 'OBS-Codesign-Password' --apple-id "${CODESIGN_IDENT_USER}" --team-id "${CODESIGN_TEAM}" --password "${CODESIGN_IDENT_PASS}"
xcrun notarytool submit "${output_name}".dmg --keychain-profile "OBS-Codesign-Password" --wait
local -i _status=0
xcrun stapler staple ${output_name}.dmg || _status=1
if (( _status )) {
log_error "Notarization failed. Use 'xcrun notarytool log <submission ID>' to check errors."
return 2
}
}
popd
} else {
log_group "Archiving obs-studio..."
pushd build_macos
XZ_OPT=-T0 tar -cvJf ${output_name}.tar.xz OBS.app
popd
}
if [[ ${config} == Release ]] {
log_group "Archiving debug symbols..."
mkdir -p build_macos/dSYMs
pushd build_macos/dSYMs
rm -rf -- *.dSYM(N)
cp -pR ${PWD:h}/**/*.dSYM .
XZ_OPT=-T0 tar -cvJf ${output_name}-dSYMs.tar.xz -- *
mv ${output_name}-dSYMs.tar.xz ${PWD:h}
popd
}
log_group
} elif [[ ${host_os} == ubuntu ]] {
local cmake_bin='/usr/bin/cmake'
local -a cmake_args=()
if (( debug )) cmake_args+=(--verbose)
if (( package )) {
log_group "Packaging obs-studio..."
pushd ${project_root}
${cmake_bin} --build build_${target%%-*} --config ${config} --target package ${cmake_args}
output_name="${output_name}-${target##*-}-ubuntu-gnu"
pushd ${project_root}/build_${target%%-*}
local -a files=(obs-studio-*-Linux*.(ddeb|deb|ddeb.sha256|deb.sha256))
for file (${files}) {
mv ${file} ${file//obs-studio-*-Linux/${output_name}}
}
popd
popd
} else {
log_group "Archiving obs-studio..."
output_name="${output_name}-${target##*-}-ubuntu-gnu"
pushd ${project_root}/build_${target%%-*}/install/${config}
XZ_OPT=-T0 tar -cvJf ${project_root}/build_${target%%-*}/${output_name}.tar.xz (bin|lib|share)
popd
}
pushd ${project_root}
${cmake_bin} --build build_${target%%-*} --config ${config} --target package_source ${cmake_args}
output_name="${output_name}-sources"
pushd ${project_root}/build_${target%%-*}
local -a files=(obs-studio-*-sources.tar.*)
for file (${files}) {
mv ${file} ${file//obs-studio-*-sources/${output_name}}
}
popd
popd
log_group
}
}
package ${@}

View File

@ -0,0 +1,89 @@
[CmdletBinding()]
param(
[ValidateSet('x64', 'arm64')]
[string] $Target = 'x64',
[ValidateSet('Debug', 'RelWithDebInfo', 'Release', 'MinSizeRel')]
[string] $Configuration = 'RelWithDebInfo'
)
$ErrorActionPreference = 'Stop'
if ( $DebugPreference -eq 'Continue' ) {
$VerbosePreference = 'Continue'
$InformationPreference = 'Continue'
}
if ( $env:CI -eq $null ) {
throw "Build-Windows.ps1 requires CI environment"
}
if ( ! ( [System.Environment]::Is64BitOperatingSystem ) ) {
throw "obs-studio requires a 64-bit system to build and run."
}
if ( $PSVersionTable.PSVersion -lt '7.2.0' ) {
Write-Warning 'The obs-studio 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 $_
Log-Group
exit 2
}
$ScriptHome = $PSScriptRoot
$ProjectRoot = Resolve-Path -Path "$PSScriptRoot/../.."
$UtilityFunctions = Get-ChildItem -Path $PSScriptRoot/utils.pwsh/*.ps1 -Recurse
foreach($Utility in $UtilityFunctions) {
Write-Debug "Loading $($Utility.FullName)"
. $Utility.FullName
}
Install-BuildDependencies -WingetFile "${ScriptHome}/.Wingetfile"
Push-Location -Stack BuildTemp
Ensure-Location $ProjectRoot
$CmakeArgs = @('--preset', "windows-ci-${Target}")
$CmakeBuildArgs = @('--build')
$CmakeInstallArgs = @()
if ( $DebugPreference -eq 'Continue' ) {
$CmakeArgs += ('--debug-output')
$CmakeBuildArgs += ('--verbose')
$CmakeInstallArgs += ('--verbose')
}
$CmakeBuildArgs += @(
'--preset', "windows-${Target}"
'--config', $Configuration
'--parallel'
'--', '/consoleLoggerParameters:Summary', '/noLogo'
)
$CmakeInstallArgs += @(
'--install', "build_${Target}"
'--prefix', "${ProjectRoot}/build_${Target}/install"
'--config', $Configuration
)
Log-Group "Configuring obs-studio..."
Invoke-External cmake @CmakeArgs
Log-Group "Building obs-studio..."
Invoke-External cmake @CmakeBuildArgs
Log-Group "Installing obs-studio..."
Invoke-External cmake @CmakeInstallArgs
Pop-Location -Stack BuildTemp
Log-Group
}
Build

View File

@ -0,0 +1,79 @@
[CmdletBinding()]
param(
[ValidateSet('x64', 'arm64')]
[string] $Target = 'x64',
[ValidateSet('Debug', 'RelWithDebInfo', 'Release', 'MinSizeRel')]
[string] $Configuration = 'RelWithDebInfo'
)
$ErrorActionPreference = 'Stop'
if ( $DebugPreference -eq 'Continue' ) {
$VerbosePreference = 'Continue'
$InformationPreference = 'Continue'
}
if ( $env:CI -eq $null ) {
throw "Package-Windows.ps1 requires CI environment"
}
if ( ! ( [System.Environment]::Is64BitOperatingSystem ) ) {
throw "obs-studio requires a 64-bit system to build and run."
}
if ( $PSVersionTable.PSVersion -lt '7.2.0' ) {
Write-Warning 'The obs-studio packaging 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/../.."
$UtilityFunctions = Get-ChildItem -Path $PSScriptRoot/utils.pwsh/*.ps1 -Recurse
foreach( $Utility in $UtilityFunctions ) {
Write-Debug "Loading $($Utility.FullName)"
. $Utility.FullName
}
Install-BuildDependencies -WingetFile "${ScriptHome}/.Wingetfile"
$GitDescription = Invoke-External git describe --tags --long
$Tokens = ($GitDescription -split '-')
$CommitVersion = $Tokens[0..$($Tokens.Count - 3)] -join '-'
$CommitHash = $($Tokens[-1]).SubString(1)
$CommitDistance = $Tokens[-2]
if ( $CommitDistance -gt 0 ) {
$OutputName = "obs-studio-${CommitVersion}-${CommitHash}"
} else {
$OutputName = "obs-studio-${CommitVersion}"
}
$CpackArgs = @(
'-C', "${Configuration}"
)
if ( $DebugPreference -eq 'Continue' ) {
$CpackArgs += ('--verbose')
}
Log-Group "Packaging obs-studio..."
Push-Location -Stack PackageTemp "build_${Target}"
cpack @CpackArgs
$Package = Get-ChildItem -filter "obs-studio-*-windows-${Target}.zip" -File
Move-Item -Path $Package -Destination "${OutputName}-windows-${Target}.zip"
Pop-Location -Stack PackageTemp
}
Package

View File

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

View File

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

View File

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

View File

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

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 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
}
$Prefixes = @{
'x64' = ${env:ProgramFiles}
'x86' = ${env:ProgramFiles(x86)}
'arm64' = ${env:ProgramFiles(arm)}
}
$Paths = $env:Path -split [System.IO.Path]::PathSeparator
$WingetOptions = @('install', '--accept-package-agreements', '--accept-source-agreements')
if ( $script:Quiet ) {
$WingetOptions += '--silent'
}
Log-Group 'Check Windows build requirements'
Get-Content $WingetFile | ForEach-Object {
$_, $Package, $_, $Path, $_, $Binary, $_, $Version = $_ -replace ',','' -split " +(?=(?:[^\']*\'[^\']*\')*[^\']*$)" -replace "'",''
$Prefixes.GetEnumerator() | ForEach-Object {
$Prefix = $_.value
$FullPath = "${Prefix}\${Path}"
if ( ( Test-Path $FullPath ) -and ! ( $Paths -contains $FullPath ) ) {
$Paths = @($FullPath) + $Paths
$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} $(if ( $Version -ne $null ) { "Version: ${Version}" } )"
if ( $Version -ne $null ) {
$WingGetOptions += @('--version', ${Version})
}
try {
$Params = $WingetOptions + $Package
winget @Params
} catch {
throw "Error while installing winget package ${Package}: $_"
}
}
}
Log-Group
}

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,149 @@
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-Group {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
[string[]] $Message
)
Process {
if ( $Env:CI -ne $null ) {
if ( $script:LogGroup ) {
Write-Output '::endgroup::'
$script:LogGroup = $false
}
if ( $Message.count -ge 1 ) {
Write-Output "::group::$($Message -join ' ')"
$script:LogGroup = $true
}
} else {
if ( $Message.count -ge 1 ) {
Log-Information $Message
}
}
}
}
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

View File

@ -0,0 +1,131 @@
import argparse
import json
import logging
import os
import sys
from typing import Any
from json_source_map import calculate
from json_source_map.errors import InvalidInputError
from jsonschema import Draft7Validator
def discover_schema_file(filename: str) -> tuple[str | None, Any]:
logger = logging.getLogger()
with open(filename) as json_file:
json_data = json.load(json_file)
schema_filename = json_data.get("$schema", None)
if not schema_filename:
logger.info(f"ℹ️ ${filename} has no schema definition")
return (None, None)
schema_file = os.path.join(os.path.dirname(filename), schema_filename)
with open(schema_file) as schema_file:
schema_data = json.load(schema_file)
return (str(schema_file), schema_data)
def validate_json_files(
schema_data: dict[Any, Any], json_file_name: str
) -> list[dict[str, str]]:
logger = logging.getLogger()
with open(json_file_name) as json_file:
text_data = json_file.read()
json_data = json.loads(text_data)
source_map = calculate(text_data)
validator = Draft7Validator(schema_data)
violations = []
for violation in sorted(validator.iter_errors(json_data), key=str):
logger.info(
f"⚠️ Schema violation in file '{json_file_name}':\n{violation}\n----\n"
)
if len(violation.absolute_path):
error_path = "/".join(
str(path_element) for path_element in violation.absolute_path
)
error_entry = source_map["/{}".format(error_path)]
violation_data = {
"file": json_file_name,
"title": "Validation Error",
"message": violation.message,
"annotation_level": "failure",
"start_line": error_entry.value_start.line + 1,
"end_line": error_entry.value_end.line + 1,
}
violations.append(violation_data)
return violations
def main() -> int:
parser = argparse.ArgumentParser(
description="Validate JSON files by schema definition"
)
parser.add_argument(
"json_files", metavar="FILE", type=str, nargs="+", help="JSON file to validate"
)
parser.add_argument(
"--loglevel", type=str, help="Set log level", default="WARNING", required=False
)
arguments = parser.parse_args()
logging.basicConfig(level=arguments.loglevel, format="%(levelname)s - %(message)s")
logger = logging.getLogger()
schema_mappings = {}
for json_file in arguments.json_files:
try:
(schema_file, schema_data) = discover_schema_file(json_file)
except OSError as e:
logger.error(f"❌ Failed to discover schema for file '{json_file}': {e}")
return 2
if schema_file and schema_file not in schema_mappings.keys():
schema_mappings.update(
{schema_file: {"schema_data": schema_data, "files": set()}}
)
schema_mappings[schema_file]["files"].add(json_file)
validation_errors = []
for schema_entry in schema_mappings.values():
for json_file in schema_entry["files"]:
try:
new_errors = validate_json_files(schema_entry["schema_data"], json_file)
except (InvalidInputError, OSError) as e:
logger.error(
f"❌ Failed to create JSON source map for file '{json_file}': {e}"
)
return 2
[validation_errors.append(error) for error in new_errors]
if validation_errors:
try:
with open("validation_errors.json", "w") as results_file:
json.dump(validation_errors, results_file)
except OSError as e:
logger.error(f"❌ Failed to write validation results file: {e}")
return 2
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,372 @@
import asyncio
import json
import os
import time
import sys
import zipfile
import aiohttp
import requests
from io import BytesIO
from typing import List, Dict
from collections import defaultdict
MINIMUM_PURGE_AGE = 9.75 * 24 * 60 * 60 # slightly less than 10 days
TIMEOUT = 10
SKIPPED_SERVICES = {"SHOWROOM", "Dacast"}
SERVICES_FILE = "plugins/rtmp-services/data/services.json"
PACKAGE_FILE = "plugins/rtmp-services/data/package.json"
CACHE_FILE = "other/timestamps.json"
GITHUB_OUTPUT_FILE = os.environ.get("GITHUB_OUTPUT", None)
DO_NOT_PING = {"jp9000"}
PR_MESSAGE = """This is an automatically created pull request to remove unresponsive servers and services.
| Service | Action Taken | Author(s) |
| ------- | ------------ | --------- |
{table}
If you are not responsible for an affected service and want to be excluded from future pings please let us know.
Created by workflow run: https://github.com/{repository}/actions/runs/{run_id}"""
# GQL is great isn't it
GQL_QUERY = """{
repositoryOwner(login: "obsproject") {
repository(name: "obs-studio") {
object(expression: "master") {
... on Commit {
blame(path: "plugins/rtmp-services/data/services.json") {
ranges {
startingLine
endingLine
commit {
author {
user {
login
}
}
}
}
}
}
}
}
}
}"""
def get_last_artifact():
s = requests.session()
s.headers["Authorization"] = f'Bearer {os.environ["GITHUB_TOKEN"]}'
run_id = os.environ["WORKFLOW_RUN_ID"]
repo = os.environ["REPOSITORY"]
# fetch run first, get workflow id from there to get workflow runs
r = s.get(f"https://api.github.com/repos/{repo}/actions/runs/{run_id}")
r.raise_for_status()
workflow_id = r.json()["workflow_id"]
r = s.get(
f"https://api.github.com/repos/{repo}/actions/workflows/{workflow_id}/runs",
params=dict(
per_page=1,
status="completed",
branch="master",
conclusion="success",
event="schedule",
),
)
r.raise_for_status()
runs = r.json()
if not runs["workflow_runs"]:
raise ValueError("No completed workflow runs found")
r = s.get(runs["workflow_runs"][0]["artifacts_url"])
r.raise_for_status()
for artifact in r.json()["artifacts"]:
if artifact["name"] == "timestamps":
artifact_url = artifact["archive_download_url"]
break
else:
raise ValueError("No previous artifact found.")
r = s.get(artifact_url)
r.raise_for_status()
zip_data = BytesIO()
zip_data.write(r.content)
with zipfile.ZipFile(zip_data) as zip_ref:
for info in zip_ref.infolist():
if info.filename == "timestamps.json":
return json.loads(zip_ref.read(info.filename))
def find_people_to_blame(raw_services: str, servers: list[tuple[str, str]]) -> dict:
if not servers:
return dict()
# Fetch Blame data from github
s = requests.session()
s.headers["Authorization"] = f'Bearer {os.environ["GITHUB_TOKEN"]}'
r = s.post(
"https://api.github.com/graphql", json=dict(query=GQL_QUERY, variables=dict())
)
r.raise_for_status()
j = r.json()
# The file is only ~2600 lines so this isn't too crazy and makes the lookup very easy
line_author = dict()
for blame in j["data"]["repositoryOwner"]["repository"]["object"]["blame"][
"ranges"
]:
for i in range(blame["startingLine"] - 1, blame["endingLine"]):
if user := blame["commit"]["author"]["user"]:
line_author[i] = user["login"]
service_authors = defaultdict(set)
for i, line in enumerate(raw_services.splitlines()):
if '"url":' not in line:
continue
for server, service in servers:
if server in line and (author := line_author.get(i)):
if author not in DO_NOT_PING:
service_authors[service].add(author)
return service_authors
def set_output(name, value):
if not GITHUB_OUTPUT_FILE:
return
try:
with open(GITHUB_OUTPUT_FILE, "a", encoding="utf-8", newline="\n") as f:
f.write(f"{name}={value}\n")
except Exception as e:
print(f"Writing to github output files failed: {e!r}")
async def check_servers_task(
session: aiohttp.ClientSession, host: str, protocol: str, servers: List[str]
) -> List[Dict]:
query = [dict(url=h, protocol=protocol) for h in servers]
async with session.get(
f"http://{host}:8999/test_remote_servers", json=query
) as resp:
return await resp.json()
async def process_services(session: aiohttp.ClientSession, check_servers: List[str]):
try:
with open(SERVICES_FILE, encoding="utf-8") as services_file:
raw_services = services_file.read()
services = json.loads(raw_services)
with open(PACKAGE_FILE, encoding="utf-8") as package_file:
package = json.load(package_file)
except OSError as e:
print(f"❌ Could not open services/package file: {e}")
return 1
# attempt to load last check result cache
try:
with open(CACHE_FILE, encoding="utf-8") as check_file:
fail_timestamps = json.load(check_file)
except OSError as e:
# cache might be evicted or not exist yet, so this is non-fatal
print(
f"⚠️ Could not read cache file, trying to get last artifact (Exception: {e})"
)
try:
fail_timestamps = get_last_artifact()
except Exception as e:
print(f"⚠️ Could not fetch cache file, starting fresh. (Exception: {e})")
fail_timestamps = dict()
else:
print("Fetched cache file from last run artifact.")
else:
print("Successfully loaded cache file:", CACHE_FILE)
start_time = int(time.time())
affected_services = dict()
removed_servers = list()
# create temporary new list
new_services = services.copy()
new_services["services"] = []
for service in services["services"]:
# skip services that do custom stuff that we can't easily check
if service["name"] in SKIPPED_SERVICES:
new_services["services"].append(service)
continue
service_type = service.get("recommended", {}).get("output", "rtmp_output")
if service_type not in {"rtmp_output", "ffmpeg_hls_muxer"}:
print("Unknown service type:", service_type)
new_services["services"].append(service)
continue
protocol = "rtmp" if service_type == "rtmp_output" else "hls"
# create a copy to mess with
new_service = service.copy()
new_service["servers"] = []
# run checks for all the servers, and store results in timestamp cache
try:
servers = [s["url"] for s in service["servers"]]
tasks = []
for host in check_servers:
tasks.append(
asyncio.create_task(
check_servers_task(session, host, protocol, servers)
)
)
results = await asyncio.gather(*tasks)
except Exception as e:
print(
f"❌ Querying server status for \"{service['name']}\" failed with: {e}"
)
return 1
# go over results
for server, result in zip(service["servers"], zip(*results)):
failure_count = sum(not res["status"] for res in result)
probe_count = len(result)
# only treat server as failed if all check servers reported a failure
is_ok = failure_count < probe_count
if not is_ok:
failures = {res["comment"] for res in result if not res["status"]}
print(
f"⚠️ Connecting to server failed: {server['url']} (Reason(s): {failures})"
)
if ts := fail_timestamps.get(server["url"], None):
if (delta := start_time - ts) >= MINIMUM_PURGE_AGE:
print(
f'🗑️ Purging server "{server["url"]}", it has been '
f"unresponsive for {round(delta/60/60/24)} days."
)
removed_servers.append((server["url"], service["name"]))
# continuing here means not adding it to the new list, thus dropping it
continue
else:
fail_timestamps[server["url"]] = start_time
elif is_ok and server["url"] in fail_timestamps:
# remove timestamp of failed check if server is back
delta = start_time - fail_timestamps[server["url"]]
print(
f'💡 Server "{server["url"]}" is back after {round(delta/60/60/24)} days!'
)
del fail_timestamps[server["url"]]
new_service["servers"].append(server)
if (diff := len(service["servers"]) - len(new_service["servers"])) > 0:
print(f'ℹ️ Removed {diff} server(s) from {service["name"]}')
affected_services[service["name"]] = f"{diff} servers removed"
# remove services with no valid servers
if not new_service["servers"]:
print(f'💀 Service "{service["name"]}" has no valid servers left, removing!')
affected_services[service["name"]] = f"Service removed"
else:
new_services["services"].append(new_service)
# wait a bit between services
await asyncio.sleep(2.0)
# write cache file
try:
os.makedirs("other", exist_ok=True)
with open(CACHE_FILE, "w", encoding="utf-8") as cache_file:
json.dump(fail_timestamps, cache_file)
except OSError as e:
print(f"❌ Could not write cache file: {e}")
return 1
else:
print("Successfully wrote cache file:", CACHE_FILE)
if removed_servers:
# increment package version and save that as well
package["version"] += 1
package["files"][0]["version"] += 1
try:
with open(SERVICES_FILE, "w", encoding="utf-8") as services_file:
json.dump(new_services, services_file, indent=4, ensure_ascii=False)
services_file.write("\n")
with open(PACKAGE_FILE, "w", encoding="utf-8") as package_file:
json.dump(package, package_file, indent=4)
package_file.write("\n")
except OSError as e:
print(f"❌ Could not write services/package file: {e}")
return 1
else:
print(
f"Successfully wrote services/package files:\n- {SERVICES_FILE}\n- {PACKAGE_FILE}"
)
# try to find authors to ping, this is optional and is allowed to fail
try:
service_authors = find_people_to_blame(raw_services, removed_servers)
except Exception as e:
print(f"⚠ Could not fetch blame for some reason: {e}")
service_authors = dict()
# set GitHub outputs
set_output("make_pr", "true")
msg = PR_MESSAGE.format(
repository=os.environ["REPOSITORY"],
run_id=os.environ["WORKFLOW_RUN_ID"],
table="\n".join(
"| {name} | {action} | {authors} |".format(
name=name.replace("|", "\\|"),
action=action,
authors=", ".join(
f"@{author}" for author in sorted(service_authors.get(name, []))
),
)
for name, action in sorted(affected_services.items())
),
)
set_output("pr_message", json.dumps(msg))
else:
set_output("make_pr", "false")
async def main():
# check for environment variables
try:
api_key = os.environ["API_KEY"]
servers = os.environ["API_SERVERS"].split(",")
if not servers:
raise ValueError("No checker servers!")
# Mask everything except the region code
for server in servers:
prefix = server[: server.index(".") + 1]
print(f"::add-mask::{prefix}")
suffix = server[server.index(".", len(prefix)) :]
print(f"::add-mask::{suffix}")
except Exception as e:
print(f"❌ Failed getting required environment variables: {e}")
return 1
# create aiohttp session
async with aiohttp.ClientSession() as session:
session.headers["Authorization"] = api_key
session.headers["User-Agent"] = "OBS Repo Service Checker/1.0"
return await process_services(session, servers)
if __name__ == "__main__":
sys.exit(asyncio.run(main()))

View File

@ -0,0 +1,22 @@
autoload -Uz is-at-least log_group log_info log_error log_status
local macos_version=$(sw_vers -productVersion)
log_group 'Install macOS build requirements'
log_info 'Checking macOS version...'
if ! is-at-least 26.0 ${macos_version}; then
log_error "Minimum required macOS version is 26.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
log_group

View File

@ -0,0 +1,54 @@
autoload -Uz log_debug log_group
log_group 'Check Ubuntu build requirements'
log_debug 'Checking Ubuntu distribution name and version...'
if [[ -f /etc/os-release ]] {
local dist_name
local dist_version
read -r dist_name dist_version <<< "$(source /etc/os-release; print "${NAME} ${VERSION_ID}")"
if [[ ${dist_name} != Ubuntu ]] {
log_error "Not running on an Ubuntu distribution. Aborting"
log_group
return 2
}
autoload -Uz is-at-least && if ! is-at-least 24.04 ${dist_version}; then
log_error "Not running on a recent-enough Ubuntu distribution. Aborting"
log_group
return 2
fi
} else {
log_error "Unable to determine local Linux distribution, but Ubuntu is required. Aborting"
log_group
return 2
}
local -a dependencies=("${(fA)$(<${SCRIPT_HOME}/.Aptfile)}")
local -a install_list
local binary
sudo apt-get update -qq
for dependency (${dependencies}) {
local -a tokens=(${=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]})
}
log_debug "List of dependencies to install: ${install_list}"
if (( #install_list )) {
sudo apt-get -y --no-install-recommends install ${install_list}
}
rehash
log_group

View File

@ -0,0 +1,84 @@
autoload -Uz log_debug log_error log_info log_status log_group log_output log_warning
local -r _usage="Usage: %B${0}%b <source> <volume name> <output_name>
Create macOS disk image <volume name> <output_name> with contents of <source>"
if (( ! # )) {
log_error 'Called without arguments.'
log_output ${_usage}
return 2
}
local source=${1}
local volume_name=${2}
local output_name=${3}
log_group "Create macOS disk image"
trap "safe_hdiutil detach /Volumes/${output_name}; rm temp.dmg; log_group; return 2" ERR
safe_hdiutil() {
local _status=0
local -r -a _backoff=(2 5 10 15 30)
for i ({1..5}) {
hdiutil ${@} && _status=0 || _status=1
if (( _status )) {
log_warning "Unable to run 'hdiutil ${@}' (attempt #${i}). Retrying."
if (( ${+CI} )) sudo pkill -9 XProtect >/dev/null || true
sleep ${_backoff[${i}]}
} else {
break
}
}
if (( _status )) {
log_error "Unable to run 'hdiutil ${@}'. Aborting"
log_group
return 2
}
}
safe_hdiutil create \
-volname "${volume_name}" \
-srcfolder ${source} \
-ov \
-fs HFS+ \
-format UDRW \
temp.dmg
safe_hdiutil attach \
-noverify \
-readwrite \
-mountpoint /Volumes/${output_name} \
temp.dmg
log_info "Waiting 2 seconds to ensure mounted volume is available..."
sleep 2
log_status "Done"
log_info "Setting up disk volume..."
log_status "Volume icon"
SetFile -c icnC /Volumes/${output_name}/.VolumeIcon.icns
log_status "Icon positions"
osascript package.applescript ${output_name}
log_status "File permissions"
chmod -Rf go-w /Volumes/${output_name}
SetFile -a C /Volumes/${output_name}
rm -rf -- /Volumes/${output_name}/.fseventsd(N)
log_info "Converting disk image..."
safe_hdiutil detach /Volumes/${output_name}
safe_hdiutil convert \
-format ULFO \
-ov \
-o ${output_name}.dmg temp.dmg
rm temp.dmg
trap '' ERR
log_group
return 0

View File

@ -0,0 +1 @@
if (( debug )) print -PR -e "::debug::%F{220}DEBUG: ${@}%f"

View File

@ -0,0 +1 @@
print -u2 -PR "::error::%F{1} ✖︎%f ${@}"

View File

@ -0,0 +1,12 @@
autoload -Uz log_info
if (( ! ${+_log_group} )) typeset -g _log_group=0
if (( _log_group )) {
print "::endgroup::"
typeset -g _log_group=0
}
if (( # )) {
print "::group::${@}"
typeset -g _log_group=1
}

View File

@ -0,0 +1 @@
print -PR "%F{4} =>%f %B${@}%b"

View File

@ -0,0 +1 @@
print -PR " ${@}"

View File

@ -0,0 +1 @@
print -PR "%F{2} >%f ${@}"

View File

@ -0,0 +1 @@
print -PR "::warning::%F{3} => ${@}%f"

View File

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

View File

@ -0,0 +1,37 @@
autoload -Uz is-at-least log_debug log_warning
if (( ${+commands[ccache]} )) {
log_debug "Found ccache at ${commands[ccache]}"
local ccache_version=$(ccache --version | head -1 | cut -d ' ' -f 3)
if ! is-at-least 4.12 ${ccache_version}; then
ccache --set-config=run_second_cpp=true
fi
ccache --set-config=direct_mode=true
ccache --set-config=inode_cache=true
ccache --set-config=compiler_check=content
ccache --set-config=file_clone=true
local -a sloppiness=(
include_file_mtime
include_file_ctime
file_stat_matches
system_headers
)
if [[ ${host_os} == macos ]] {
sloppiness+=(
modules
clang_index_store
)
ccache --set-config=sloppiness=${(j:,:)sloppiness}
}
ccache --set-config=cache_dir="${GITHUB_WORKSPACE:-${HOME}}/.ccache"
ccache --set-config=max_size="${CCACHE_SIZE:-1G}"
ccache -z > /dev/null
} else {
log_warning "Ccache not available"
}

View File

@ -0,0 +1,94 @@
autoload -Uz log_group log_error log_status log_info log_debug
if (( ! ${+commands[curl]} )) {
log_error 'curl not available.'
return 2
}
if (( ! ${+commands[jq]} )) {
log_error 'jq not available.'
return 2
}
local -a curl_opts=(--show-error --silent --location -O ${@})
pushd ${project_root}
log_group 'Installing obs-studio build dependencies...'
mkdir -p ${project_root}/.deps
local deps_version
local deps_baseurl
local deps_label
local deps_hash
IFS=';' read -r deps_version deps_baseurl deps_label deps_hash deps_revision <<< \
"$(jq -r --arg target "${target}" '
.configurePresets[]
| select(.name=="dependencies")
| .vendor["obsproject.com/obs-studio"].dependencies["cef"]
| {version, baseUrl, "label", "hash": .hashes[$target], "revision": .revision[$target]}
| join(";")
' ${project_root}/CMakePresets.json)"
if (( ! deps_version )) {
log_error 'No valid cef spec found in buildspec.json.'
return 2
}
log_group 'Setting up pre-built Chromium Embedded Framework...'
pushd ${project_root}/.deps
local _filename="cef_binary_${deps_version}_${target//ubuntu-/linux_}${deps_revision:+"_v${deps_revision}"}.tar.xz"
local _url=${deps_baseurl}/${_filename}
local _target="cef_binary_${deps_version}_${target//ubuntu-/linux_}"
typeset -g CEF_VERSION=${deps_version}
log_debug "Running curl ${curl_opts} ${_url}"
curl ${curl_opts} ${_url} && \
log_status "Downloaded ${deps_label} for ${target}."
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."
mkdir -p ${_target} && pushd ${_target}
XZ_OPT=-T0 tar --strip-components 1 -xJf ../${_filename} && log_status "${deps_label} extracted."
popd
log_group 'Installing obs-studio build dependencies from apt...'
sudo apt-get install -y --no-install-recommends \
build-essential libglib2.0-dev \
extra-cmake-modules lsb-release dh-cmake \
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 \
libpipewire-0.3-dev \
libpulse-dev \
libx264-dev \
libmbedtls-dev \
libgl1-mesa-dev \
libjansson-dev \
uthash-dev \
libsimde-dev \
libluajit-5.1-dev python3-dev \
libx11-dev libxcb-randr0-dev libxcb-shm0-dev libxcb-xinerama0-dev \
libxcb-composite0-dev libxinerama-dev libxcb1-dev libx11-xcb-dev libxcb-xfixes0-dev \
swig libcmocka-dev libxss-dev libglvnd-dev \
libxkbcommon-dev libatk1.0-dev libatk-bridge2.0-dev libxcomposite-dev libxdamage-dev \
libasound2-dev libfdk-aac-dev libfontconfig-dev libfreetype6-dev libjack-jackd2-dev \
libpulse-dev libsndio-dev libspeexdsp-dev libudev-dev libv4l-dev libva-dev libvlc-dev \
libpci-dev libdrm-dev \
nlohmann-json3-dev libwebsocketpp-dev libasio-dev libqrcodegencpp-dev \
libffmpeg-nvenc-dev librist-dev libsrt-openssl-dev \
qt6-base-dev libqt6svg6-dev qt6-base-private-dev \
libvpl-dev libvpl2

View File

@ -0,0 +1,176 @@
name: Analyze Project
on:
workflow_call:
jobs:
windows:
name: Windows 🪟 (PVS-Studio)
runs-on: windows-2025-vs2026
if: false && github.repository_owner == 'obsproject'
defaults:
run:
shell: pwsh
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
fetch-depth: 0
- name: Build OBS Studio 🧱
uses: ./.github/actions/build-obs
env:
TWITCH_CLIENTID: ${{ secrets.TWITCH_CLIENT_ID }}
TWITCH_HASH: ${{ secrets.TWITCH_HASH }}
RESTREAM_CLIENTID: ${{ secrets.RESTREAM_CLIENTID }}
RESTREAM_HASH: ${{ secrets.RESTREAM_HASH }}
YOUTUBE_CLIENTID: ${{ secrets.YOUTUBE_CLIENTID }}
YOUTUBE_CLIENTID_HASH: ${{ secrets.YOUTUBE_CLIENTID_HASH }}
YOUTUBE_SECRET: ${{ secrets.YOUTUBE_SECRET }}
YOUTUBE_SECRET_HASH: ${{ secrets.YOUTUBE_SECRET_HASH }}
GPU_PRIORITY_VAL: ${{ secrets.GPU_PRIORITY_VAL }}
with:
target: x64
config: Debug
- name: Run PVS-Studio Analysis
uses: ./.github/actions/windows-analysis
with:
pvsUsername: ${{ secrets.PVS_NAME }}
pvsKey: ${{ secrets.PVS_KEY }}
target: x64
config: Debug
macos:
name: macOS 🍏 (clang-analyze)
runs-on: macos-26
defaults:
run:
shell: zsh --no-rcs --errexit --pipefail {0}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
fetch-depth: 0
- name: Set Up Environment 🔧
id: setup
run: |
: Set Up Environment 🔧
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
print '::group::Enable Xcode 26.5'
sudo xcode-select --switch /Applications/Xcode_26.5.app/Contents/Developer
print '::endgroup::'
print '::group::Clean Homebrew Environment'
local -a unwanted_formulas=()
local -a remove_formulas=()
for formula (${unwanted_formulas}) {
if [[ -d ${HOMEBREW_PREFIX}/Cellar/${formula} ]] remove_formulas+=(${formula})
}
if (( #remove_formulas )) brew uninstall --ignore-dependencies ${remove_formulas}
print '::endgroup::'
local xcode_cas_path="${HOME}/Library/Developer/Xcode/DerivedData/CompilationCache.noindex"
if ! [[ -d ${xcode_cas_path} ]] mkdir -p ${xcode_cas_path}
print "xcodeCasPath=${xcode_cas_path}" >> $GITHUB_OUTPUT
- name: Set Up Code Signing 🔑
uses: ./.github/actions/setup-macos-codesigning
id: codesign
with:
codesignIdentity: ${{ secrets.MACOS_SIGNING_IDENTITY }}
codesignCertificate: ${{ secrets.MACOS_SIGNING_CERT }}
certificatePassword: ${{ secrets.MACOS_SIGNING_CERT_PASSWORD }}
keychainPassword: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
provisioningProfile: ${{ secrets.MACOS_SIGNING_PROVISIONING_PROFILE }}
notarizationUser: ${{ secrets.MACOS_NOTARIZATION_USERNAME }}
notarizationPassword: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }}
- name: Build OBS Studio 🧱
env:
TWITCH_CLIENTID: ${{ secrets.TWITCH_CLIENT_ID }}
TWITCH_HASH: ${{ secrets.TWITCH_HASH }}
RESTREAM_CLIENTID: ${{ secrets.RESTREAM_CLIENTID }}
RESTREAM_HASH: ${{ secrets.RESTREAM_HASH }}
YOUTUBE_CLIENTID: ${{ secrets.YOUTUBE_CLIENTID }}
YOUTUBE_CLIENTID_HASH: ${{ secrets.YOUTUBE_CLIENTID_HASH }}
YOUTUBE_SECRET: ${{ secrets.YOUTUBE_SECRET }}
YOUTUBE_SECRET_HASH: ${{ secrets.YOUTUBE_SECRET_HASH }}
CODESIGN_IDENT: ${{ steps.codesign.outputs.codesignIdent }}
CODESIGN_TEAM: ${{ steps.codesign.outputs.codesignTeam }}
PROVISIONING_PROFILE: ${{ steps.codesign.outputs.provisioningProfileUUID }}
XCODE_CAS_PATH: ${{ steps.setup.outputs.xcodeCasPath }}
run: |
: Run macOS Build
local -a build_args=(
--config Debug
--target macos-arm64
--codesign
--analyze
)
if (( ${+RUNNER_DEBUG} )) build_args+=(--debug)
git fetch origin --no-tags --no-recurse-submodules -q
.github/scripts/build-macos ${build_args}
- name: Compile Analytics Data 📊
run: |
: Compile Analytics Data 📊
local analytics_root='${{ github.workspace }}/analytics'
local -a analytics_files=(${analytics_root}/StaticAnalyzer/obs-studio/**/*.plist)
for file (${analytics_files}) {
mv ${file} ${analytics_root}/${${file:t}//plist/sarif}
}
pushd ${analytics_root}
# The jq expression below merges the results of all separate compilation (and
# analysis) runs into a single result of all tools, thus meeting the
# requirements of the codeql-action's upload job.
# The merged sarif file contains a single "runs" array with a single
# corresponding "tool" object.
#
# Each sarif file will have its own "rules" array (depending on which rules the
# file violated), so all violated rules across all compilations need to be
# collected into this array.
#
# Next, all "artifacts" array elements and "rules" array elements from all
# sarif files have to be added to the same "global" corresponding arrays in the
# new single "runs" object.
#
# Finally, because LLVM produces codeflow regions with invalid "endLine" and
# "endColumn" entries (with value 0), those entries are removed from the
# regions in the results' codeFlows arrays.
jq -s '{
"$schema": first(.[]."$schema"),
"version": first(.[].version),
"runs": [{
"tool": {
"driver": (first(.[].runs[].tool.driver) | del(.rules)) + {
"rules": reduce(.[].runs[].tool.driver.rules) as $obj ([]; . + $obj) | unique
}
},
"artifacts": reduce(.[].runs[].artifacts) as $obj ([]; . + $obj) | unique,
"results": (reduce(.[].runs[].results) as $obj ([]; . + $obj))
| del(
.[].codeFlows[].threadFlows[].locations[].location.physicalLocation.region.endLine,
.[].codeFlows[].threadFlows[].locations[].location.physicalLocation.region.endColumn
| select(. == 0)
)
}]
}' *.sarif > merged.sarif
popd
- name: Upload SARIF report files 📦
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
sarif_file: "${{ github.workspace }}/analytics/merged.sarif"
category: 'clang-analyze (macOS Apple Silicon)'

View File

@ -0,0 +1,428 @@
name: Build Project
on:
workflow_call:
jobs:
check-event:
name: Event Data 🔎
runs-on: ubuntu-26.04
defaults:
run:
shell: bash
outputs:
package: ${{ steps.setup.outputs.package }}
codesign: ${{ steps.setup.outputs.codesign }}
notarize: ${{ steps.setup.outputs.notarize }}
config: ${{ steps.setup.outputs.config }}
commitHash: ${{ steps.setup.outputs.commitHash }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Check Event Data ☑️
id: setup
env:
GH_TOKEN: ${{ github.token }}
run: |
: Check Event Data ☑️
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
case "${GITHUB_EVENT_NAME}" in
pull_request)
config_data=('codesign:false' 'notarize:false' 'package:false' 'config:RelWithDebInfo')
if gh pr view ${{ github.event.number }} --json labels \
| jq -e -r '.labels[] | select(.name == "Seeking Testers")' > /dev/null; then
config_data[0]='codesign:true'
config_data[2]='package:true'
fi
;;
push)
config_data=('codesign:true' 'notarize:false' 'package:true' 'config:RelWithDebInfo')
if [[ ${GITHUB_REF_NAME} =~ [0-9]+.[0-9]+.[0-9]+(-(rc|beta).+)? ]]; then
config_data[1]='notarize:true'
config_data[3]='config:Release'
fi
;;
workflow_dispatch)
config_data=('codesign:true' 'notarize:false' 'package:false' 'config:RelWithDebInfo')
;;
schedule)
config_data=('codesign:true' 'notarize:false' 'package:true' 'config:RelWithDebInfo')
;;
*) ;;
esac
for config in "${config_data[@]}"; do
IFS=':' read -r key value <<< "${config}"
echo "${key}=${value}" >> $GITHUB_OUTPUT
done
echo "commitHash=${GITHUB_SHA:0:9}" >> $GITHUB_OUTPUT
macos-build:
name: macOS 🍏
runs-on: macos-26
needs: check-event
strategy:
fail-fast: false
matrix:
target: [arm64, x86_64]
defaults:
run:
shell: zsh --no-rcs --errexit --pipefail {0}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
fetch-depth: 0
- name: Set Up Environment 🔧
id: setup
run: |
: Set Up Environment 🔧
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
print '::group::Enable Xcode 26.5'
sudo xcode-select --switch /Applications/Xcode_26.5.app/Contents/Developer
print '::endgroup::'
print '::group::Clean Homebrew Environment'
local -a unwanted_formulas=()
local -a remove_formulas=()
for formula (${unwanted_formulas}) {
if [[ -d ${HOMEBREW_PREFIX}/Cellar/${formula} ]] remove_formulas+=(${formula})
}
if (( #remove_formulas )) brew uninstall --ignore-dependencies ${remove_formulas}
print '::endgroup::'
local -A arch_names=(x86_64 intel arm64 apple)
print "cpuName=${arch_names[${{ matrix.target }}]}" >> $GITHUB_OUTPUT
local xcode_cas_path="${HOME}/Library/Developer/Xcode/DerivedData/CompilationCache.noindex"
if ! [[ -d ${xcode_cas_path} ]] mkdir -p ${xcode_cas_path}
print "xcodeCasPath=${xcode_cas_path}" >> $GITHUB_OUTPUT
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
id: xcode-cache
with:
path: ${{ steps.setup.outputs.xcodeCasPath }}
key: ${{ runner.os }}-xcode-${{ matrix.target }}-${{ needs.check-event.outputs.config }}
restore-keys: |
${{ runner.os }}-xcode-${{ matrix.target }}-
- name: Set Up Code Signing 🔑
uses: ./.github/actions/setup-macos-codesigning
if: fromJSON(needs.check-event.outputs.codesign)
id: codesign
with:
codesignIdentity: ${{ secrets.MACOS_SIGNING_IDENTITY }}
codesignCertificate: ${{ secrets.MACOS_SIGNING_CERT }}
certificatePassword: ${{ secrets.MACOS_SIGNING_CERT_PASSWORD }}
keychainPassword: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
provisioningProfile: ${{ secrets.MACOS_SIGNING_PROVISIONING_PROFILE }}
notarizationUser: ${{ secrets.MACOS_NOTARIZATION_USERNAME }}
notarizationPassword: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }}
- name: Build OBS Studio 🧱
uses: ./.github/actions/build-obs
env:
TWITCH_CLIENTID: ${{ secrets.TWITCH_CLIENT_ID }}
TWITCH_HASH: ${{ secrets.TWITCH_HASH }}
RESTREAM_CLIENTID: ${{ secrets.RESTREAM_CLIENTID }}
RESTREAM_HASH: ${{ secrets.RESTREAM_HASH }}
YOUTUBE_CLIENTID: ${{ secrets.YOUTUBE_CLIENTID }}
YOUTUBE_CLIENTID_HASH: ${{ secrets.YOUTUBE_CLIENTID_HASH }}
YOUTUBE_SECRET: ${{ secrets.YOUTUBE_SECRET }}
YOUTUBE_SECRET_HASH: ${{ secrets.YOUTUBE_SECRET_HASH }}
XCODE_CAS_PATH: ${{ steps.setup.outputs.xcodeCasPath }}
with:
target: ${{ matrix.target }}
config: ${{ needs.check-event.outputs.config }}
codesign: ${{ fromJSON(needs.check-event.outputs.codesign) }}
codesignIdent: ${{ steps.codesign.outputs.codesignIdent }}
codesignTeam: ${{ steps.codesign.outputs.codesignTeam }}
provisioningProfileUUID: ${{ steps.codesign.outputs.provisioningProfileUUID }}
- name: Package OBS Studio 📀
uses: ./.github/actions/package-obs
if: ${{ fromJSON(needs.check-event.outputs.package) }}
with:
target: ${{ matrix.target }}
config: ${{ needs.check-event.outputs.config }}
package: ${{ fromJSON(needs.check-event.outputs.package) }}
codesign: ${{ fromJSON(needs.check-event.outputs.codesign) && fromJSON(steps.codesign.outputs.haveCodesignIdent) }}
codesignIdent: ${{ steps.codesign.outputs.codesignIdent }}
notarize: ${{ fromJSON(needs.check-event.outputs.notarize) && fromJSON(steps.codesign.outputs.haveNotarizationUser) }}
codesignUser: ${{ secrets.MACOS_NOTARIZATION_USERNAME }}
codesignPass: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }}
- name: Upload Artifacts 📡
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ fromJSON(needs.check-event.outputs.package) }}
with:
name: obs-studio-macos-${{ matrix.target }}-${{ needs.check-event.outputs.commitHash }}
path: ${{ github.workspace }}/build_macos/obs-studio-*-macos-${{ steps.setup.outputs.cpuName }}.*
- name: Upload Debug Symbol Artifacts 🪲
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ needs.check-event.outputs.config == 'Release' }}
with:
name: obs-studio-macos-${{ matrix.target }}-${{ needs.check-event.outputs.commitHash }}-dSYMs
path: ${{ github.workspace }}/build_macos/obs-studio-*-macos-${{ steps.setup.outputs.cpuName }}-dSYMs.tar.xz
- uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
if: github.event_name != 'pull_request' && steps.xcode-cache.outputs.cache-hit != 'true'
with:
path: ${{ steps.setup.outputs.xcodeCasPath }}
key: ${{ runner.os }}-xcode-${{ matrix.target }}-${{ needs.check-event.outputs.config }}
ubuntu-build:
name: Ubuntu 🐧
strategy:
fail-fast: false
matrix:
os: [ubuntu-26.04]
runs-on: ${{ matrix.os }}
needs: check-event
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
fetch-depth: 0
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
id: ccache-cache
with:
path: ${{ github.workspace }}/.ccache
key: ${{ runner.os }}-${{ matrix.os }}-ccache-x86_64-${{ needs.check-event.outputs.config }}
restore-keys: |
${{ runner.os }}-${{ matrix.os }}-ccache-x86_64-
- name: Build OBS Studio 🧱
uses: ./.github/actions/build-obs
env:
TWITCH_CLIENTID: ${{ secrets.TWITCH_CLIENT_ID }}
TWITCH_HASH: ${{ secrets.TWITCH_HASH }}
RESTREAM_CLIENTID: ${{ secrets.RESTREAM_CLIENTID }}
RESTREAM_HASH: ${{ secrets.RESTREAM_HASH }}
YOUTUBE_CLIENTID: ${{ secrets.YOUTUBE_CLIENTID }}
YOUTUBE_CLIENTID_HASH: ${{ secrets.YOUTUBE_CLIENTID_HASH }}
YOUTUBE_SECRET: ${{ secrets.YOUTUBE_SECRET }}
YOUTUBE_SECRET_HASH: ${{ secrets.YOUTUBE_SECRET_HASH }}
with:
target: x86_64
config: ${{ needs.check-event.outputs.config }}
- name: Package OBS Studio 📀
uses: ./.github/actions/package-obs
if: ${{ fromJSON(needs.check-event.outputs.package) }}
with:
target: x86_64
config: ${{ needs.check-event.outputs.config }}
package: ${{ fromJSON(needs.check-event.outputs.package) }}
- name: Upload Source Tarball 🗜️
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ fromJSON(needs.check-event.outputs.package) }}
with:
name: obs-studio-${{ matrix.os }}-sources-${{ needs.check-event.outputs.commitHash }}
path: ${{ github.workspace }}/build_ubuntu/obs-studio-*-sources.*
- name: Upload Artifacts 📡
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ fromJSON(needs.check-event.outputs.package) }}
with:
name: obs-studio-${{ matrix.os }}-x86_64-${{ needs.check-event.outputs.commitHash }}
path: ${{ github.workspace }}/build_ubuntu/obs-studio-*-x86_64-ubuntu-gnu.*
- name: Upload Debug Symbol Artifacts 🪲
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ fromJSON(needs.check-event.outputs.package) }}
with:
name: obs-studio-${{ matrix.os }}-x86_64-${{ needs.check-event.outputs.commitHash }}-dbgsym
path: ${{ github.workspace }}/build_ubuntu/obs-studio-*-x86_64-ubuntu-gnu-dbgsym.ddeb
- uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
if: github.event_name != 'pull_request' && steps.ccache-cache.outputs.cache-hit != 'true'
with:
path: ${{ github.workspace }}/.ccache
key: ${{ runner.os }}-${{ matrix.os }}-ccache-x86_64-${{ needs.check-event.outputs.config }}
flatpak-build:
name: Flatpak 📦
runs-on: ubuntu-26.04
needs: check-event
defaults:
run:
shell: bash
env:
FLATPAK_BUILD_SHARE_PATH: flatpak_app/files/share
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-25.08
options: --privileged
volumes:
- /usr/local/lib/android:/to_clean/android
- /opt/hostedtoolcache/CodeQL:/to_clean/codeql
- /usr/local/.ghcup:/to_clean/ghcup
- /opt/hostedtoolcache/Python:/to_clean/python
- /usr/share/swift:/to_clean/swift
- /usr/share/dotnet:/to_clean/dotnet
steps:
- name: Prepare build space
shell: bash
run: |
: Prepare build space
echo ::group::Available storage
df -h
echo ::endgroup::
echo ::group::Remove Android stuff
rm -rf /to_clean/android/*
echo ::endgroup::
echo ::group::Remove CodeQL stuff
rm -rf /to_clean/codeql/*
echo ::endgroup::
echo ::group::Remove GHCup stuff
rm -rf /to_clean/ghcup/*
echo ::endgroup::
echo ::group::Remove Python stuff
rm -rf /to_clean/python/*
echo ::endgroup::
echo ::group::Remove Swift stuff
rm -rf /to_clean/swift/*
echo ::endgroup::
echo ::group::Remove .NET stuff
rm -rf /to_clean/dotnet/*
echo ::endgroup::
echo ::group::Available storage
df -h
echo ::endgroup::
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
fetch-depth: 0
set-safe-directory: ${{ env.GITHUB_WORKSPACE }}
- name: Set Up Environment 🔧
id: setup
env:
GH_TOKEN: ${{ github.token }}
run: |
: Set Up Environment 🔧
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
cache_key='flatpak-builder-${{ hashFiles('build-aux/com.obsproject.Studio.json') }}'
cache_ref='master'
read -r id key size unit created accessed <<< \
"$(gh cache list --ref "refs/heads/${cache_ref}" --key "${cache_key}-x86_64" | head -1)"
if [[ "${key}" ]]; then
echo "cacheHit=true" >> $GITHUB_OUTPUT
else
echo "cacheHit=false" >> $GITHUB_OUTPUT
fi
echo "cacheKey=${cache_key}" >> $GITHUB_OUTPUT
- name: Validate Flatpak manifest
uses: ./.github/actions/flatpak-builder-lint
with:
artifact: manifest
path: build-aux/com.obsproject.Studio.json
- name: Build Flatpak Manifest 🧾
uses: flatpak/flatpak-github-actions/flatpak-builder@401fe28a8384095fc1531b9d320b292f0ee45adb # v6.7
with:
build-bundle: ${{ fromJSON(needs.check-event.outputs.package) }}
bundle: obs-studio-flatpak-${{ needs.check-event.outputs.commitHash }}.flatpak
manifest-path: ${{ github.workspace }}/build-aux/com.obsproject.Studio.json
cache: ${{ fromJSON(steps.setup.outputs.cacheHit) || (github.event_name == 'push' && github.ref_name == 'master')}}
restore-cache: ${{ fromJSON(steps.setup.outputs.cacheHit) }}
cache-key: ${{ steps.setup.outputs.cacheKey }}
mirror-screenshots-url: https://dl.flathub.org/media
- name: Validate AppStream
uses: ./.github/actions/flatpak-builder-lint
with:
artifact: appstream
path: ${{ env.FLATPAK_BUILD_SHARE_PATH }}/metainfo/com.obsproject.Studio.metainfo.xml
- name: Verify Icon and Metadata in app-info
working-directory: ${{ env.FLATPAK_BUILD_SHARE_PATH }}
run: |
: Verify Icon and Metadata in app-info
test -f app-info/icons/flatpak/128x128/com.obsproject.Studio.png || { echo "::error::Missing 128x128 icon in app-info"; exit 1; }
test -f app-info/xmls/com.obsproject.Studio.xml.gz || { echo "::error::Missing com.obsproject.Studio.xml.gz in app-info"; exit 1; }
- name: Validate build directory
uses: ./.github/actions/flatpak-builder-lint
with:
artifact: builddir
path: flatpak_app
- name: Validate repository
uses: ./.github/actions/flatpak-builder-lint
with:
artifact: repo
path: repo
windows-build:
name: Windows 🪟
strategy:
matrix:
architecture: [x64, arm64]
runs-on: windows-2025-vs2026
needs: check-event
defaults:
run:
shell: pwsh
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
fetch-depth: 0
- name: Build OBS Studio 🧱
uses: ./.github/actions/build-obs
env:
TWITCH_CLIENTID: ${{ secrets.TWITCH_CLIENT_ID }}
TWITCH_HASH: ${{ secrets.TWITCH_HASH }}
RESTREAM_CLIENTID: ${{ secrets.RESTREAM_CLIENTID }}
RESTREAM_HASH: ${{ secrets.RESTREAM_HASH }}
YOUTUBE_CLIENTID: ${{ secrets.YOUTUBE_CLIENTID }}
YOUTUBE_CLIENTID_HASH: ${{ secrets.YOUTUBE_CLIENTID_HASH }}
YOUTUBE_SECRET: ${{ secrets.YOUTUBE_SECRET }}
YOUTUBE_SECRET_HASH: ${{ secrets.YOUTUBE_SECRET_HASH }}
GPU_PRIORITY_VAL: ${{ secrets.GPU_PRIORITY_VAL }}
with:
target: ${{ matrix.architecture }}
config: ${{ needs.check-event.outputs.config }}
- name: Package OBS Studio 📀
uses: ./.github/actions/package-obs
if: ${{ fromJSON(needs.check-event.outputs.package) }}
with:
target: ${{ matrix.architecture }}
config: ${{ needs.check-event.outputs.config }}
package: ${{ fromJSON(needs.check-event.outputs.package) }}
- name: Upload Artifacts 📡
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ fromJSON(needs.check-event.outputs.package) }}
with:
name: obs-studio-windows-${{ matrix.architecture }}-${{ needs.check-event.outputs.commitHash }}
path: ${{ github.workspace }}/build_${{ matrix.architecture }}/obs-studio-*-windows-${{ matrix.architecture }}.zip

View File

@ -0,0 +1,63 @@
name: Check Code Formatting 🛠️
on:
workflow_call:
jobs:
clang-format:
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: clang-format Check 🐉
id: clang-format
uses: ./.github/actions/run-clang-format
with:
failCondition: error
swift-format:
runs-on: macos-26
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: swift-format Check 🔥
id: swift-format
uses: ./.github/actions/run-swift-format
with:
failCondition: error
gersemi:
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: gersemi Check 🎛️
id: gersemi
uses: ./.github/actions/run-gersemi
with:
failCondition: error
flatpak-validator:
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Flatpak Manifest Check 📦
id: flatpak-check
uses: ./.github/actions/flatpak-manifest-validator
with:
failCondition: error
qt-xml-validator:
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Qt XML Check 🖼️
id: qt-xml-check
uses: ./.github/actions/qt-xml-validator
with:
failCondition: error

View File

@ -0,0 +1,151 @@
name: Dispatch
run-name: Dispatched Repository Actions - ${{ inputs.job }} ⌛️
on:
workflow_dispatch:
inputs:
job:
description: Dispatch job to run
required: true
type: choice
options:
- steam
- services
- translations
- documentation
- patches
ref:
description: GitHub reference to use for job
type: string
required: false
customAssetWindows:
description: Custom Windows build for Steam Upload
type: string
required: false
customAssetMacOSApple:
description: Custom macOS Apple Silicon build for Steam Upload
type: string
required: false
customAssetMacOSIntel:
description: Custom macOS Intel build for Steam Upload
type: string
required: false
channel:
description: Channel to use when generating Windows update files
type: string
required: false
permissions:
contents: write
jobs:
services-validation:
name: Validate Services 🕵️
if: github.repository_owner == 'obsproject' && inputs.job == 'services'
runs-on: macos-26
permissions:
checks: write
contents: write
pull-requests: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check for Defunct Services 📉
uses: ./.github/actions/services-validator
with:
repositorySecret: ${{ secrets.GITHUB_TOKEN }}
checkApiSecret: ${{ secrets.CHECK_SERVERS_API_KEY }}
checkApiServers: ${{ secrets.CHECK_SERVERS_LIST }}
runSchemaChecks: true
runServiceChecks: true
createPullRequest: true
download-language-files:
name: Download Language Files 🌐
if: github.repository_owner == 'obsproject' && inputs.job == 'translations'
runs-on: ubuntu-26.04
env:
CROWDIN_PAT: ${{ secrets.CROWDIN_SYNC_CROWDIN_PAT }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
token: ${{ secrets.CROWDIN_SYNC_GITHUB_PAT }}
fetch-depth: 0
- uses: obsproject/obs-crowdin-sync/download@4b488c7ced03aa109d9f12529bd91c26c54b3e89
steam-upload:
name: Upload Steam Builds 🚂
if: github.repository_owner == 'obsproject' && inputs.job == 'steam'
runs-on: macos-26
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/steam-upload
with:
steamSecret: ${{ secrets.STEAM_SHARED_SECRET }}
steamUser: ${{ secrets.STEAM_USER }}
steamPassword: ${{ secrets.STEAM_PASSWORD }}
tagName: ${{ inputs.ref }}
customAssetWindows: ${{ inputs.customAssetWindows }}
customAssetMacOSApple: ${{ inputs.customAssetMacOSApple }}
customAssetMacOSIntel: ${{ inputs.customAssetMacOSIntel }}
workflowSecret: ${{ github.token }}
preview: false
update-documentation:
name: Update Documentation 📖
if: github.repository_owner == 'obsproject' && inputs.job == 'documentation'
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/generate-docs
update-documentation-cloudflare:
name: Update Documentation for Cloudflare ☁️
if: github.repository_owner == 'obsproject' && inputs.job == 'documentation'
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/generate-docs
with:
disableLinkExtensions: true
deploy-documentation:
name: Deploy Documentation to Cloudflare ☁️
if: github.repository_owner == 'obsproject' && inputs.job == 'documentation'
runs-on: ubuntu-26.04
needs: update-documentation-cloudflare
defaults:
run:
shell: bash
environment:
name: cf-pages-deploy
steps:
- name: Get Commit Information 🆔
id: setup
run: |
: Get Commit Hash 🆔
echo "commitHash=${GITHUB_SHA:0:9}" >> $GITHUB_OUTPUT
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: OBS Studio Docs (No Extensions) ${{ steps.setup.outputs.commitHash }}
path: docs
- name: Publish to Live Page
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
with:
workingDirectory: docs
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
command: pages publish . --project-name=${{ vars.CF_PAGES_PROJECT }} --commit-hash='${{ steps.setup.outputs.commitHash }}'
windows-patches:
name: Create Windows Patches 🩹
if: github.repository_owner == 'obsproject' && inputs.job == 'patches'
runs-on: windows-2025-vs2026
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/windows-patches
with:
tagName: ${{ inputs.ref }}
workflowSecret: ${{ github.token }}
channel: ${{ inputs.channel }}
gcsAccessKeyId: ${{ secrets.GCS_ACCESS_KEY_ID }}
gcsAccessKeySecret: ${{ secrets.GCS_ACCESS_KEY_SECRET }}

View File

@ -0,0 +1,94 @@
name: Pull
run-name: ${{ github.event.pull_request.title }} pull request run 🚀
on:
workflow_dispatch:
pull_request:
paths-ignore:
- '**.md'
branches: [master]
types: [ opened, synchronize, reopened ]
permissions:
contents: read
concurrency:
group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
cancel-in-progress: true
jobs:
check-format:
name: Format 🔍
uses: ./.github/workflows/check-format.yaml
permissions:
contents: read
build-project:
name: Build 🧱
uses: ./.github/workflows/build-project.yaml
secrets: inherit
permissions:
contents: read
compatibility-validation:
name: Validate Compatibility 🕵️
if: github.base_ref == 'master'
runs-on: ubuntu-26.04
permissions:
checks: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Check for Changed Files ✅
uses: ./.github/actions/check-changes
id: checks
with:
baseRef: origin/${{ github.base_ref }}
checkGlob: plugins/win-capture/data/*.json
- name: Check for Invalid Compatibility Data 📉
if: fromJSON(steps.checks.outputs.hasChangedFiles)
uses: ./.github/actions/compatibility-validator
services-validation:
name: Validate Services 🕵️
if: github.base_ref == 'master'
runs-on: ubuntu-26.04
permissions:
checks: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Check for Changed Files ✅
uses: ./.github/actions/check-changes
id: checks
with:
baseRef: origin/${{ github.base_ref }}
checkGlob: plugins/rtmp-services/data/*.json
- name: Check Services JSON Schema 📉
if: fromJSON(steps.checks.outputs.hasChangedFiles)
uses: ./.github/actions/services-validator
with:
repositorySecret: ${{ secrets.GITHUB_TOKEN }}
runSchemaChecks: true
runServiceChecks: false
update-documentation:
name: Update Documentation 📖
if: github.repository_owner == 'obsproject' && github.base_ref == 'master'
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Check for Changed Files ✅
uses: ./.github/actions/check-changes
id: checks
with:
baseRef: origin/${{ github.base_ref }}
checkGlob: docs/sphinx
- uses: ./.github/actions/generate-docs
if: fromJSON(steps.checks.outputs.hasChangedFiles)

View File

@ -0,0 +1,334 @@
name: Publish
run-name: Publish Repository Actions 🛫
on:
release:
types:
- published
branches:
- master
- 'release/**'
permissions:
contents: read
concurrency:
group: '${{ github.workflow }} @ ${{ github.head_ref || github.ref }}'
cancel-in-progress: true
jobs:
check-tag:
name: Check Release Tag
if: github.repository_owner == 'obsproject'
runs-on: ubuntu-26.04
outputs:
validTag: ${{ steps.check.outputs.validTag }}
flatpakMatrix: ${{ steps.check.outputs.flatpakMatrix }}
updateChannel: ${{ steps.check.outputs.updateChannel }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
fetch-depth: 0
set-safe-directory: ${{ env.GITHUB_WORKSPACE }}
- name: Check Release Tag ☑️
id: check
env:
GH_TOKEN: ${{ github.token }}
run: |
: Check Release Tag ☑️
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
shopt -s extglob
case "${GITHUB_REF_NAME}" in
+([0-9]).+([0-9]).+([0-9]) )
lastPreRelease="$(gh release list --exclude-drafts --limit 10 --json "publishedAt,tagName,isPrerelease" \
--jq '[.[] | select(.isPrerelease == true)] | first | .tagName')"
currentRelease="${GITHUB_REF_NAME}"
isPreReleaseAhead=false
printf '%s\n%s\n' "${currentRelease}" "${lastPreRelease}" | sort --version-sort --reverse --check=quiet &&
isPreReleaseAhead=false || isPreReleaseAhead=true
# Edge case: Sort considers the non-suffixed version older than a suffixed one
if ${isPreReleaseAhead} && [[ "${currentRelease}" == "${lastPreRelease//-*}" ]]; then
isPreReleaseAhead=false
fi
echo 'validTag=true' >> $GITHUB_OUTPUT
if ! ${isPreReleaseAhead}; then
echo 'flatpakMatrix=["beta", "stable"]' >> $GITHUB_OUTPUT
else
echo 'flatpakMatrix=["stable"]' >> $GITHUB_OUTPUT
fi
echo 'updateChannel=stable' >> $GITHUB_OUTPUT
;;
+([0-9]).+([0-9]).+([0-9])-@(beta|rc)*([0-9]) )
echo 'validTag=true' >> $GITHUB_OUTPUT
echo 'flatpakMatrix=["beta"]' >> $GITHUB_OUTPUT
echo 'updateChannel=beta' >> $GITHUB_OUTPUT
;;
*) echo 'validTag=false' >> $GITHUB_OUTPUT ;;
esac
flatpak-publish:
name: Flathub 📦
needs: check-tag
if: github.repository_owner == 'obsproject' && fromJSON(needs.check-tag.outputs.validTag)
runs-on: ubuntu-26.04
defaults:
run:
shell: bash
env:
FLATPAK_BUILD_SHARE_PATH: flatpak_app/files/share
TWITCH_CLIENTID: ${{ secrets.TWITCH_CLIENT_ID }}
TWITCH_HASH: ${{ secrets.TWITCH_HASH }}
RESTREAM_CLIENTID: ${{ secrets.RESTREAM_CLIENTID }}
RESTREAM_HASH: ${{ secrets.RESTREAM_HASH }}
YOUTUBE_CLIENTID: ${{ secrets.YOUTUBE_CLIENTID }}
YOUTUBE_CLIENTID_HASH: ${{ secrets.YOUTUBE_CLIENTID_HASH }}
YOUTUBE_SECRET: ${{ secrets.YOUTUBE_SECRET }}
YOUTUBE_SECRET_HASH: ${{ secrets.YOUTUBE_SECRET_HASH }}
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-25.08
options: --privileged
volumes:
- /usr/local/lib/android:/to_clean/android
- /opt/hostedtoolcache/CodeQL:/to_clean/codeql
- /usr/local/.ghcup:/to_clean/ghcup
- /opt/hostedtoolcache/Python:/to_clean/python
- /usr/share/swift:/to_clean/swift
- /usr/share/dotnet:/to_clean/dotnet
strategy:
matrix:
branch: ${{ fromJSON(needs.check-tag.outputs.flatpakMatrix) }}
steps:
- name: Prepare build space
shell: bash
run: |
: Prepare build space
echo ::group::Available storage
df -h
echo ::endgroup::
echo ::group::Remove Android stuff
rm -rf /to_clean/android/*
echo ::endgroup::
echo ::group::Remove CodeQL stuff
rm -rf /to_clean/codeql/*
echo ::endgroup::
echo ::group::Remove GHCup stuff
rm -rf /to_clean/ghcup/*
echo ::endgroup::
echo ::group::Remove Python stuff
rm -rf /to_clean/python/*
echo ::endgroup::
echo ::group::Remove Swift stuff
rm -rf /to_clean/swift/*
echo ::endgroup::
echo ::group::Remove .NET stuff
rm -rf /to_clean/dotnet/*
echo ::endgroup::
echo ::group::Available storage
df -h
echo ::endgroup::
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
fetch-depth: 0
set-safe-directory: ${{ env.GITHUB_WORKSPACE }}
- name: Set Up Environment 🔧
id: setup
env:
GH_TOKEN: ${{ github.token }}
run: |
: Set Up Environment 🔧
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
cache_key='flatpak-builder-${{ hashFiles('build-aux/com.obsproject.Studio.json') }}'
cache_ref='master'
read -r id key size unit created accessed <<< \
"$(gh cache list --ref "refs/heads/${cache_ref}" --key "${cache_key}-x86_64" | head -1)"
if [[ "${key}" ]]; then
echo "cacheHit=true" >> $GITHUB_OUTPUT
else
echo "cacheHit=false" >> $GITHUB_OUTPUT
fi
echo "cacheKey=${cache_key}" >> $GITHUB_OUTPUT
echo "commitHash=${GITHUB_SHA:0:9}" >> $GITHUB_OUTPUT
- name: Validate Flatpak manifest
uses: ./.github/actions/flatpak-builder-lint
with:
artifact: manifest
path: build-aux/com.obsproject.Studio.json
- name: Build Flatpak Manifest
uses: flatpak/flatpak-github-actions/flatpak-builder@401fe28a8384095fc1531b9d320b292f0ee45adb # v6.7
with:
build-bundle: false
manifest-path: ${{ github.workspace }}/build-aux/com.obsproject.Studio.json
cache: ${{ fromJSON(steps.setup.outputs.cacheHit) }}
cache-key: ${{ steps.setup.outputs.cacheKey }}
mirror-screenshots-url: https://dl.flathub.org/media
branch: ${{ matrix.branch }}
- name: Validate AppStream
uses: ./.github/actions/flatpak-builder-lint
with:
artifact: appstream
path: ${{ env.FLATPAK_BUILD_SHARE_PATH }}/metainfo/com.obsproject.Studio.metainfo.xml
- name: Verify Icon and Metadata in app-info
working-directory: ${{ env.FLATPAK_BUILD_SHARE_PATH }}
run: |
: Verify Icon and Metadata in app-info
test -f app-info/icons/flatpak/128x128/com.obsproject.Studio.png || { echo "::error::Missing 128x128 icon in app-info"; exit 1; }
test -f app-info/xmls/com.obsproject.Studio.xml.gz || { echo "::error::Missing com.obsproject.Studio.xml.gz in app-info"; exit 1; }
- name: Validate build directory
uses: ./.github/actions/flatpak-builder-lint
with:
artifact: builddir
path: flatpak_app
- name: Validate repository
uses: ./.github/actions/flatpak-builder-lint
with:
artifact: repo
path: repo
- name: Publish to Flathub Beta
uses: flatpak/flatpak-github-actions/flat-manager@401fe28a8384095fc1531b9d320b292f0ee45adb # v6.7
if: ${{ matrix.branch == 'beta' }}
with:
flat-manager-url: https://hub.flathub.org/
repository: beta
token: ${{ secrets.FLATHUB_BETA_TOKEN }}
build-log-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: Publish to Flathub
uses: flatpak/flatpak-github-actions/flat-manager@401fe28a8384095fc1531b9d320b292f0ee45adb # v6.7
if: ${{ matrix.branch == 'stable' }}
with:
flat-manager-url: https://hub.flathub.org/
repository: stable
token: ${{ secrets.FLATHUB_TOKEN }}
build-log-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
steam-upload:
name: Upload Steam Builds 🚂
needs: check-tag
if: github.repository_owner == 'obsproject' && fromJSON(needs.check-tag.outputs.validTag)
runs-on: macos-26
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/steam-upload
with:
steamSecret: ${{ secrets.STEAM_SHARED_SECRET }}
steamUser: ${{ secrets.STEAM_USER }}
steamPassword: ${{ secrets.STEAM_PASSWORD }}
workflowSecret: ${{ github.token }}
tagName: ${{ github.ref_name }}
preview: false
windows-patches:
name: Create Windows Patches 🩹
needs: check-tag
if: github.repository_owner == 'obsproject' && fromJSON(needs.check-tag.outputs.validTag)
runs-on: windows-2025-vs2026
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/windows-patches
with:
tagName: ${{ github.ref_name }}
workflowSecret: ${{ github.token }}
channel: ${{ needs.check-tag.outputs.updateChannel }}
gcsAccessKeyId: ${{ secrets.GCS_ACCESS_KEY_ID }}
gcsAccessKeySecret: ${{ secrets.GCS_ACCESS_KEY_SECRET }}
create-appcast:
name: Create Sparkle Appcast 🎙️
if: github.repository_owner == 'obsproject' && fromJSON(needs.check-tag.outputs.validTag)
runs-on: macos-26
needs: check-tag
strategy:
fail-fast: false
matrix:
target: [arm64, x86_64]
defaults:
run:
shell: zsh --no-rcs --errexit --pipefail {0}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
fetch-depth: 0
ref: ${{ github.ref }}
- name: Set Up Environment 🔧
id: setup
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
: Set Up Environment 🔧
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
local channel='stable'
if [[ ${GITHUB_REF_NAME} == *(beta|rc)* ]] {
channel='beta'
}
local -A arch_names=(x86_64 intel arm64 apple)
print "cpuName=${arch_names[${{ matrix.target }}]}" >> $GITHUB_OUTPUT
print "commitHash=${GITHUB_SHA:0:9}" >> $GITHUB_OUTPUT
print "channel=${channel}" >> $GITHUB_OUTPUT
local workflowRunId="$(gh run list --branch "${GITHUB_REF_NAME}" --event push --limit 1 --json "databaseId" --jq ".[].databaseId")"
print "workflowRunId=${workflowRunId}" >> $GITHUB_OUTPUT
- name: Download Artifact 📥
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: obs-studio-macos-${{ matrix.target }}-${{ steps.setup.outputs.commitHash }}
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ steps.setup.outputs.workflowRunId }}
- name: Generate Appcast 🎙️
id: generate-appcast
uses: ./.github/actions/sparkle-appcast
with:
sparklePrivateKey: ${{ secrets.SPARKLE_PRIVATE_KEY }}
baseImage: ${{ github.workspace }}/obs-studio-*-macos-${{ steps.setup.outputs.cpuName }}.dmg
channel: ${{ steps.setup.outputs.channel }}
count: 5
urlPrefix: 'https://cdn-fastly.obsproject.com/downloads'
customTitle: 'OBS Studio'
customLink: 'https://obsproject.com/'
- name: Upload Artifacts 📡
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: macos-sparkle-update-${{ matrix.target }}
path: ${{ github.workspace }}/output
merge-appcasts:
runs-on: ubuntu-26.04
needs: create-appcast
steps:
- name: Merge Appcasts
uses: actions/upload-artifact/merge@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: macos-sparkle-update
pattern: macos-sparkle-update-*
delete-merged: true

265
obs-studio/.github/workflows/push.yaml vendored Normal file
View File

@ -0,0 +1,265 @@
name: Push
run-name: ${{ github.ref_name }} push run 🚀
on:
push:
paths-ignore:
- '**.md'
branches:
- master
- 'release/**'
tags:
- '*'
permissions:
contents: write
jobs:
check-format:
name: Format 🔍
if: github.ref_name == 'master'
uses: ./.github/workflows/check-format.yaml
permissions:
contents: read
build-project:
name: Build 🧱
uses: ./.github/workflows/build-project.yaml
secrets: inherit
permissions:
contents: read
compatibility-validation:
name: Validate Compatibility 🕵️
if: github.ref_name == 'master'
runs-on: ubuntu-26.04
permissions:
checks: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Check for Changed Files ✅
uses: ./.github/actions/check-changes
id: checks
with:
baseRef: ${{ github.event.before }}
checkGlob: plugins/win-capture/data/*.json
- name: Check for Invalid Compatibility Data 📉
if: fromJSON(steps.checks.outputs.hasChangedFiles)
uses: ./.github/actions/compatibility-validator
with:
repositorySecret: ${{ github.token }}
services-validation:
name: Validate Services 🕵️
if: github.ref_name == 'master'
runs-on: ubuntu-26.04
permissions:
checks: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Check for Changed Files ✅
uses: ./.github/actions/check-changes
id: checks
with:
baseRef: ${{ github.event.before }}
checkGlob: plugins/rtmp-services/data/*.json
- name: Check Services JSON Schema 📉
if: fromJSON(steps.checks.outputs.hasChangedFiles)
uses: ./.github/actions/services-validator
with:
repositorySecret: ${{ github.token }}
runSchemaChecks: true
runServiceChecks: false
update-documentation:
name: Update Documentation 📖
if: github.repository_owner == 'obsproject' && (github.ref_name == 'master' || github.ref_type == 'tag')
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Check for Changed Files ✅
if: github.ref_type != 'tag'
uses: ./.github/actions/check-changes
id: checks
with:
baseRef: ${{ github.event.before }}
checkGlob: '!(cmake*)'
- uses: ./.github/actions/generate-docs
if: github.ref_type == 'tag' || fromJSON(steps.checks.outputs.hasChangedFiles)
with:
disableLinkExtensions: ${{ github.ref_type == 'tag' }}
deploy-documentation:
name: Deploy Documentation to Cloudflare ☁️
if: github.repository_owner == 'obsproject' && github.ref_type == 'tag'
runs-on: ubuntu-26.04
needs: update-documentation
defaults:
run:
shell: bash
environment:
name: cf-pages-deploy
steps:
- name: Get Commit Information 🆔
id: setup
run: |
: Get Commit Hash 🆔
echo "commitHash=${GITHUB_SHA:0:9}" >> $GITHUB_OUTPUT
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: OBS Studio Docs (No Extensions) ${{ steps.setup.outputs.commitHash }}
path: docs
- name: Set Up Redirects 🔄
run: |
: Set Up Redirects 🔄
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
echo "/previous/27.2 https://obsproject.com/docs/27.2 302" >> docs/_redirects
echo "/previous/:major.:minor https://:major-:minor.${{ vars.CF_PAGES_PROJECT }}.pages.dev 302" >> docs/_redirects
- name: Publish to Live Page
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
with:
workingDirectory: docs
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
command: pages publish . --project-name=${{ vars.CF_PAGES_PROJECT }} --commit-hash='${{ steps.setup.outputs.commitHash }}'
sign-windows-build:
name: Windows Signing ✍️
uses: obsproject/obs-studio/.github/workflows/sign-windows.yaml@ac19ea663375cd02997be92b7d8ff6dd89a511ad
if: github.repository_owner == 'obsproject' && github.ref_type == 'tag'
needs: build-project
permissions:
contents: 'read'
id-token: 'write'
attestations: 'write'
secrets: inherit
create-release:
name: Create Release 🛫
if: github.ref_type == 'tag'
runs-on: ubuntu-26.04
needs: [build-project, sign-windows-build]
defaults:
run:
shell: bash
steps:
- name: Check Release Tag ☑️
id: check
run: |
: Check Release Tag ☑️
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
shopt -s extglob
case "${GITHUB_REF_NAME}" in
+([0-9]).+([0-9]).+([0-9]) )
echo 'validTag=true' >> $GITHUB_OUTPUT
echo 'prerelease=false' >> $GITHUB_OUTPUT
echo "version=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
;;
+([0-9]).+([0-9]).+([0-9])-@(beta|rc)*([0-9]) )
echo 'validTag=true' >> $GITHUB_OUTPUT
echo 'prerelease=true' >> $GITHUB_OUTPUT
echo "version=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
;;
*) echo 'validTag=false' >> $GITHUB_OUTPUT ;;
esac
- name: Download Build Artifacts 📥
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
if: ${{ fromJSON(steps.check.outputs.validTag) }}
- name: Rename Files 🏷️
if: fromJSON(steps.check.outputs.validTag)
run: |
: Rename Files 🏷️
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
root_dir="${PWD}"
commit_hash="${GITHUB_SHA:0:9}"
macos_arm64_artifact_name="obs-studio-macos-arm64-${commit_hash}"
macos_arm64_dsym_artifact_name="obs-studio-macos-arm64-${commit_hash}-dSYMs"
macos_intel_artifact_name="obs-studio-macos-x86_64-${commit_hash}"
macos_intel_dsym_artifact_name="obs-studio-macos-x86_64-${commit_hash}-dSYMs"
ubuntu_2604_x86_64_artifact_name="obs-studio-ubuntu-26.04-x86_64-${commit_hash}"
ubuntu_2604_x86_64_debug_name="obs-studio-ubuntu-26.04-x86_64-${commit_hash}-dbgsym"
ubuntu_2604_sources_name="obs-studio-ubuntu-26.04-sources-${commit_hash}"
windows_x64_artifact_name="obs-studio-windows-x64-${{ steps.check.outputs.version }}-signed"
windows_x64_installer_name="obs-studio-windows-x64-${{ steps.check.outputs.version }}-installer"
windows_x64_debug_name="obs-studio-windows-x64-${{ steps.check.outputs.version }}-pdbs"
windows_arm64_artifact_name="obs-studio-windows-arm64-${{ steps.check.outputs.version }}-signed"
windows_arm64_debug_name="obs-studio-windows-arm64-${{ steps.check.outputs.version }}-pdbs"
echo '::group::Renaming Artifacts'
mv -v "${macos_arm64_artifact_name}/"obs-studio-*-macos-apple.dmg \
"${root_dir}"/OBS-Studio-${{ steps.check.outputs.version }}-macOS-Apple.dmg
mv -v "${macos_arm64_dsym_artifact_name}/"obs-studio-*-macos-apple-dSYMs.tar.xz \
"${root_dir}"/OBS-Studio-${{ steps.check.outputs.version }}-macOS-Apple-dSYMs.tar.xz
mv -v "${macos_intel_artifact_name}/"obs-studio-*-macos-intel.dmg \
"${root_dir}"/OBS-Studio-${{ steps.check.outputs.version }}-macOS-Intel.dmg
mv -v "${macos_intel_dsym_artifact_name}/"obs-studio-*-macos-intel-dSYMs.tar.xz \
"${root_dir}"/OBS-Studio-${{ steps.check.outputs.version }}-macOS-Intel-dSYMs.tar.xz
mv -v "${ubuntu_2604_x86_64_artifact_name}/"obs-studio-*-x86_64-ubuntu-gnu.deb \
"${root_dir}"/OBS-Studio-${{ steps.check.outputs.version }}-Ubuntu-26.04-x86_64.deb
mv -v "${ubuntu_2604_x86_64_debug_name}/"obs-studio-*-x86_64-ubuntu-gnu-dbgsym.ddeb \
"${root_dir}"/OBS-Studio-${{ steps.check.outputs.version }}-Ubuntu-26.04-x86_64-dbsym.ddeb
mv -v "${ubuntu_2604_sources_name}/"obs-studio-*-sources.tar.gz \
"${root_dir}"/OBS-Studio-${{ steps.check.outputs.version }}-Sources.tar.gz
mv -v "${windows_x64_artifact_name}/"OBS-Studio-*.zip \
"${root_dir}"/OBS-Studio-${{ steps.check.outputs.version }}-Windows-x64.zip
mv -v "${windows_x64_installer_name}/"OBS-Studio-*.exe \
"${root_dir}"/OBS-Studio-${{ steps.check.outputs.version }}-Windows-x64-Installer.exe
mv -v "${windows_x64_debug_name}/"OBS-Studio-*-pdbs.zip \
"${root_dir}"/OBS-Studio-${{ steps.check.outputs.version }}-Windows-x64-PDBs.zip
mv -v "${windows_arm64_artifact_name}/"OBS-Studio-*.zip \
"${root_dir}"/OBS-Studio-${{ steps.check.outputs.version }}-Windows-arm64.zip
mv -v "${windows_arm64_debug_name}/"OBS-Studio-*-pdbs.zip \
"${root_dir}"/OBS-Studio-${{ steps.check.outputs.version }}-Windows-arm64-PDBs.zip
echo '::endgroup::'
- name: Generate Checksums 🪪
if: fromJSON(steps.check.outputs.validTag)
run: |
: Generate Checksums 🪪
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
shopt -s extglob
echo "### Checksums" > ${{ github.workspace }}/CHECKSUMS.txt
for file in ${{ github.workspace }}/@(*.deb|*.ddeb|*.dmg|*.tar.xz|*.tar.gz|*.exe|*.zip); do
echo " ${file##*/}: $(sha256sum "${file}" | cut -d " " -f 1)" >> ${{ github.workspace }}/CHECKSUMS.txt
done
- name: Create Release 🛫
if: fromJSON(steps.check.outputs.validTag)
id: create_release
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
with:
draft: true
prerelease: ${{ fromJSON(steps.check.outputs.prerelease) }}
tag_name: ${{ steps.check.outputs.version }}
name: OBS Studio ${{ steps.check.outputs.version }}
body_path: ${{ github.workspace }}/CHECKSUMS.txt
files: |
${{ github.workspace }}/OBS-Studio-${{ steps.check.outputs.version }}-macOS-*.dmg
${{ github.workspace }}/OBS-Studio-${{ steps.check.outputs.version }}-macOS-*-dSYMs.tar.xz
${{ github.workspace }}/OBS-Studio-${{ steps.check.outputs.version }}-Ubuntu-*.deb
${{ github.workspace }}/OBS-Studio-${{ steps.check.outputs.version }}-Ubuntu-*.ddeb
${{ github.workspace }}/OBS-Studio-${{ steps.check.outputs.version }}-Sources.tar.gz
${{ github.workspace }}/OBS-Studio-${{ steps.check.outputs.version }}-Windows-x64.zip
${{ github.workspace }}/OBS-Studio-${{ steps.check.outputs.version }}-Windows-x64-Installer.exe
${{ github.workspace }}/OBS-Studio-${{ steps.check.outputs.version }}-Windows-x64-PDBs.zip
${{ github.workspace }}/OBS-Studio-${{ steps.check.outputs.version }}-Windows-arm64.zip
${{ github.workspace }}/OBS-Studio-${{ steps.check.outputs.version }}-Windows-arm64-PDBs.zip

View File

@ -0,0 +1,205 @@
name: Scheduled
run-name: Scheduled Repository Actions ⏰
on:
workflow_dispatch:
schedule:
- cron: 17 0 * * *
permissions:
contents: write
concurrency:
group: '${{ github.workflow }} @ ${{ github.head_ref || github.ref }}'
cancel-in-progress: true
jobs:
services-availability:
name: Check Service Availability 🛜
if: github.repository_owner == 'obsproject'
runs-on: macos-26
permissions:
checks: write
contents: write
pull-requests: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Check for Defunct Services 📉
uses: ./.github/actions/services-validator
with:
repositorySecret: ${{ secrets.GITHUB_TOKEN }}
checkApiSecret: ${{ secrets.CHECK_SERVERS_API_KEY }}
checkApiServers: ${{ secrets.CHECK_SERVERS_LIST }}
runSchemaChecks: false
runServiceChecks: true
createPullRequest: true
cache-cleanup:
name: Cache Cleanup 🧹
runs-on: ubuntu-26.04
permissions:
actions: write
steps:
- name: Remove Stale Ccache Caches
env:
GH_TOKEN: ${{ github.token }}
run: |
: Remove Stale Ccache Caches
# The jq expressions below use multiple 'select' calls to filter
# each item in the array with the 'actions_caches' key.
# First it only selects objects whose 'ref' element has the value
# 'refs/heads/master', of those objects only those whose 'key'
# value matches the specified expression, before finally only
# selecting the 'id' and 'key' elements for a new object.
# The final 'join' command combines both elements with a semicolon
# into a raw string which can then be parsed directly.
echo '::group::Processing master branch cache entries'
while IFS=";" read -r cache_id cache_name; do
if [[ "${cache_name}" ]]; then
result=true
gh api -X DELETE repos/${GITHUB_REPOSITORY}/actions/caches?key=${cache_name} --jq '.total_count' &> /dev/null || result=false
if ${result}; then
echo "Deleted cache entry ${cache_name}"
else
echo "::warning::Unable to delete cache entry ${cache_name}"
fi
fi
done <<< \
"$(gh api repos/${GITHUB_REPOSITORY}/actions/caches \
--jq '.actions_caches.[] | select(.ref|test("refs/heads/master")) | select(.key|test(".*-(ccache|xcode)-*")) | {id, key} | join(";")')"
echo '::endgroup::'
echo '::group::Processing pull request cache entries'
while IFS=";" read -r cache_id cache_name cache_ref; do
if [[ "${cache_name}" ]]; then
result=true
gh api -X DELETE repos/${GITHUB_REPOSITORY}/actions/caches?key=${cache_name} --jq '.total_count' &> /dev/null || result=false
pr_number=$(echo ${cache_ref} | cut -d '/' -f 3)
if ${result}; then
echo "Deleted PR #${pr_number} cache entry ${cache_name}"
else
echo "::warning::Unable to delete PR #${pr_number} cache entry ${cache_name}"
fi
fi
done <<< \
"$(gh api repos/${GITHUB_REPOSITORY}/actions/caches \
--jq '.actions_caches.[] | select(.ref|test("refs/heads/master")|not) | select(.key|test(".*-ccache-*")) | {id, key, ref} | join(";")')"
echo '::endgroup::'
build-project:
name: Build 🧱
uses: ./.github/workflows/build-project.yaml
needs: cache-cleanup
secrets: inherit
analyze-project:
name: Analyze 🔬
uses: ./.github/workflows/analyze-project.yaml
needs: cache-cleanup
secrets: inherit
permissions:
security-events: write
upload-language-files:
name: Upload Language Files 🌐
if: github.repository_owner == 'obsproject' && github.ref_name == 'master'
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
fetch-depth: 0
- name: Check Nightly Runs ☑️
id: nightly-checks
env:
GH_TOKEN: ${{ github.token }}
run: |
: Check Nightly Runs ☑️
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
# This 'gh' command retrieves the last 2 runs of the workflow defined
# by 'scheduled.yaml' and retrieves just the 'headSha' value of the
# JSON response payload.
#
# As this job runs in context of the same workflow, the first element
# of the workflow list will be the currently active run.
#
# The jq expression then selects the 'headSha' element of the second
# element in the array, which is the SHA-1 hash of the commit used
# for the immediately prior run of this workflow.
last_nightly=$(gh run list --workflow scheduled.yaml --limit 2 --json headSha --jq '.[1].headSha')
if [[ "${GITHUB_SHA}" == "${last_nightly}" ]]; then
echo "passed=false" >> $GITHUB_OUTPUT
else
echo "passed=true" >> $GITHUB_OUTPUT
echo "lastNightly=${last_nightly}" >> $GITHUB_OUTPUT
fi
- name: Check for Changed Files ✅
if: fromJSON(steps.nightly-checks.outputs.passed)
uses: ./.github/actions/check-changes
id: checks
with:
baseRef: ${{ steps.nightly-checks.outputs.lastNightly }}
checkGlob: '**/en-US.ini'
- name: Upload US English Language Files 🇺🇸
if: steps.checks.outcome == 'success' && fromJSON(steps.checks.outputs.hasChangedFiles)
uses: obsproject/obs-crowdin-sync/upload@4b488c7ced03aa109d9f12529bd91c26c54b3e89
env:
CROWDIN_PAT: ${{ secrets.CROWDIN_SYNC_CROWDIN_PAT }}
GITHUB_EVENT_BEFORE: ${{ steps.nightly-checks.outputs.lastNightly }}
steam-upload:
name: Upload Steam Builds 🚂
needs: [build-project]
if: github.repository_owner == 'obsproject'
runs-on: macos-26
defaults:
run:
shell: zsh --no-rcs --errexit --pipefail {0}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check Nightly Runs ☑️
id: checks
env:
GH_TOKEN: ${{ github.token }}
run: |
: Check Nightly Runs ☑️
if (( ${+RUNNER_DEBUG} )) setopt XTRACE
# This 'gh' command retrieves the last 2 runs of the workflow defined
# by 'scheduled.yaml' and retrieves just the 'headSha' value of the
# JSON response payload.
#
# As this job runs in context of the same workflow, the first element
# of the workflow list will be the currently active run.
#
# The jq expression then selects the 'headSha' element of the second
# element in the array, which is the SHA-1 hash of the commit used
# for the immediately prior run of this workflow.
local last_nightly=$(gh run list --workflow scheduled.yaml --limit 2 --json headSha --jq '.[1].headSha')
if [[ "${GITHUB_SHA}" == "${last_nightly}" ]] {
print "passed=false" >> $GITHUB_OUTPUT
} else {
print "passed=true" >> $GITHUB_OUTPUT
}
- uses: ./.github/actions/steam-upload
if: fromJSON(steps.checks.outputs.passed)
with:
steamSecret: ${{ secrets.STEAM_SHARED_SECRET }}
steamUser: ${{ secrets.STEAM_USER }}
steamPassword: ${{ secrets.STEAM_PASSWORD }}
workflowSecret: ${{ secrets.GITHUB_TOKEN }}
preview: ${{ github.repository_owner != 'obsproject' }}

View File

@ -0,0 +1,84 @@
name: Sign Windows Project
on:
workflow_call:
jobs:
create-windows-update:
name: Sign Windows Build 🥩
strategy:
matrix:
architecture: [x64, arm64]
runs-on: windows-2025-vs2026
environment:
name: bouf
defaults:
run:
shell: pwsh
steps:
- name: Parse JWT
id: jwt
run: |
$token = ConvertTo-SecureString -String ${env:ACTIONS_ID_TOKEN_REQUEST_TOKEN} -AsPlainText
$jwt = Invoke-WebRequest -Uri "${env:ACTIONS_ID_TOKEN_REQUEST_URL}&audience=ignore" -Authentication Bearer -Token $token
$claim_b64 = (($jwt.Content | ConvertFrom-Json -AsHashtable).value -split '\.')[1]
$mod = $claim_b64.Length % 4
if ($mod -gt 0) {$claim_b64 += '=' * (4 - $mod)}
$claim = [Text.Encoding]::Utf8.GetString([Convert]::FromBase64String($claim_b64)) | ConvertFrom-Json -AsHashtable
$sha = ${claim}.job_workflow_sha
Write-Output "Workflow SHA: ${sha}"
"workflow_sha=${sha}" >> $env:GITHUB_OUTPUT
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: "repo"
fetch-depth: 0
ref: ${{ steps.jwt.outputs.workflow_sha }}
- name: Set Up Environment 🔧
id: setup
run: |
$channel = if ($env:GITHUB_REF_NAME -match "(beta|rc)") { "beta" } else { "stable" }
$shortHash = $env:GITHUB_SHA.Substring(0,9)
"channel=${channel}" >> $env:GITHUB_OUTPUT
"commitHash=${shortHash}" >> $env:GITHUB_OUTPUT
- name: Download Artifact 📥
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: obs-studio-windows-${{ matrix.architecture }}-${{ steps.setup.outputs.commitHash }}
path: ${{ github.workspace }}/build
- name: Run bouf 🥩
uses: ./repo/.github/actions/windows-signing
with:
gcpWorkloadIdentityProvider: ${{ secrets.GCP_IDENTITY_POOL }}
gcpServiceAccountName: ${{ secrets.GCP_SERVICE_ACCOUNT_NAME }}
version: ${{ github.ref_name }}
channel: ${{ steps.setup.outputs.channel }}
architecture: ${{ matrix.architecture }}
- name: Generate artifact attestation
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-path: ${{ github.workspace }}/output/*-${{ matrix.architecture }}.zip
- name: Upload Signed Build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: obs-studio-windows-${{ matrix.architecture }}-${{ github.ref_name }}-signed
compression-level: 0
path: ${{ github.workspace }}/output/*-${{ matrix.architecture }}.zip
- name: Upload PDBs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: obs-studio-windows-${{ matrix.architecture }}-${{ github.ref_name }}-pdbs
compression-level: 0
path: ${{ github.workspace }}/output/*-pdbs.zip
- name: Upload Installer
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: matrix.architecture == 'x64'
with:
name: obs-studio-windows-${{ matrix.architecture }}-${{ github.ref_name }}-installer
compression-level: 0
path: ${{ github.workspace }}/output/*.exe

51
obs-studio/.gitignore vendored Normal file
View File

@ -0,0 +1,51 @@
# Exclude everything
/*
# Except for default project files
!/.github
!/build-aux
!/cmake
!/deps
!/docs
!/frontend
!/libobs*
!/plugins
!/shared
!/test
!.cirrus.xml
!.clang-format
!.editorconfig
!.gersemirc
!.git-blame-ignore-devs
!.gitmodules
!.gitignore
!.mailmap
!.swift-format
!AUTHORS
!buildspec.json
!CMakeLists.txt
!CMakePresets.json
!COC.rst
!COMMITMENT
!CONTRIBUTING.md
!CODESTYLE.md
!COPYING
!INSTALL
!README.rst
# Exclude lock files
*.lock.json
# Exclude files generated by Sphinx in-tree
/docs/sphinx/_build/*
!/docs/sphinx/_build/.gitignore
!/docs/sphinx/Makefile
# Exclude modified Flatpak files
build-aux/flatpak-github-action-modified-*
# Exclude macOS legacy resource forks
.DS_Store
# Exclude CMake build number cache
/cmake/.CMakeBuildNumber

9
obs-studio/.gitmodules vendored Normal file
View File

@ -0,0 +1,9 @@
[submodule "plugins/win-dshow/libdshowcapture"]
path = deps/libdshowcapture/src
url = https://github.com/obsproject/libdshowcapture.git
[submodule "plugins/obs-browser"]
path = plugins/obs-browser
url = https://github.com/obsproject/obs-browser.git
[submodule "plugins/obs-websocket"]
path = plugins/obs-websocket
url = https://github.com/obsproject/obs-websocket.git

107
obs-studio/.mailmap Normal file
View File

@ -0,0 +1,107 @@
#
# Manually defined
#
Lain <lain@obsproject.com>
Lain <obs.jim@gmail.com>
Benjamin Klettbach <b.klettbach@gmail.com>
BtbN <btbn@btbn.de>
kc5nra <jrb@turrettech.com>
HomeWorld <homeworld@gmail.com> hwdro <pdarvaru@yahoo.com>
Michael Fabian 'Xaymar' Dirks <info@xaymar.com> <Xaymar@users.noreply.github.com>
Michael Fabian 'Xaymar' Dirks <info@xaymar.com> <michael.dirks@xaymar.com>
Michael Fabian 'Xaymar' Dirks <info@xaymar.com>
Martell Malone <martellmalone@gmail.com>
Ford Smith <fordmsmith13@gmail.com> <xtreameprogram2@gmail.com>
Ford Smith <fordmsmith13@gmail.com>
Sebastian Beckmann <beckmann.sebastian@outlook.de>
Rodney <xlnedder@gmail.com>
Reboot <reboot@gmx.ch>
#
# Generated to prevent people with multiple names or emails appearing multiple times, for the manual changes made see below:
#
jpark37 <jpark37@users.noreply.github.com> <jpark37@gmail.com>
jpark37 <jpark37@users.noreply.github.com>
DDRBoxman <colin@recursivepenguin.com> <ddrboxman@Colins-MacBook-Pro.local>
DDRBoxman <colin@recursivepenguin.com>
R1CH <r1ch@r1ch.net> <r.stanway@gmail.com>
R1CH <r1ch@r1ch.net> <notr1ch@users.noreply.github.com>
R1CH <r1ch@r1ch.net>
WizardCM <matt@obsproject.com> <matt@wizardcm.com>
WizardCM <matt@obsproject.com> <git@wizardcm.com>
WizardCM <matt@obsproject.com>
Zachary Lund <admin@computerquip.com> <zachary.lund@streamlabs.com>
Zachary Lund <admin@computerquip.com> <Zachary L1und>
Zachary Lund <admin@computerquip.com>
pkv <pkv.stream@gmail.com> <pkv@obsproject.com>
pkv <pkv.stream@gmail.com>
PatTheMav <patthemav+github@gmail.com> <PatTheMav@users.noreply.github.com>
PatTheMav <patthemav+github@gmail.com>
brittneysclark <brittney.s.clark@intel.com> <brittney.s.clark@gmail.com>
brittneysclark <brittney.s.clark@intel.com>
craftwar <craftwar@users.noreply.github.com> <coo.ky@hotmail.com>
craftwar <craftwar@users.noreply.github.com>
SCG82 <scg082+github@gmail.com> <scg082@gmail.com>
SCG82 <scg082+github@gmail.com>
Ed Maste <emaste@FreeBSD.org> <emaste@freebsd.org>
Ed Maste <emaste@FreeBSD.org>
mntone <sd8@live.jp> <mntone@outlook,jp>
mntone <sd8@live.jp>
juvester <juvester@cs.helsinki.fi> <juvester@users.noreply.github.com>
juvester <juvester@cs.helsinki.fi>
Socapex <philippe.groarke@gmail.com> <philippe_groarke@yahoo.ca>
Socapex <philippe.groarke@gmail.com>
Rat <52238970+ratwithacompiler@users.noreply.github.com> <37424386+ratwithashotgun@users.noreply.github.com>
Rat <52238970+ratwithacompiler@users.noreply.github.com>
Quinn Damerell <qdamere@microsoft.com> <quinnd@outlook.com>
Quinn Damerell <qdamere@microsoft.com>
Manuel Kroeber <manuel.kroeber@googlemail.com> <manuel.kroeber@gmail.com>
Manuel Kroeber <manuel.kroeber@googlemail.com>
Ford Smith <fordmsmith13@gmail.com> <fms34@case.edu>
Ford Smith <fordmsmith13@gmail.com>
Vainock <contact.vainock@gmail.com> <Vainock@users.noreply.github.com>
Vainock <contact.vainock@gmail.com> <39059951+Vainock@users.noreply.github.com>
Vainock <contact.vainock@gmail.com>
Wahaj Dar <mail@wahajdar.com> <wahajdar@outlook.com>
Wahaj Dar <mail@wahajdar.com>
David Cooper <david@dtcooper.com> <davidc@eventbrite.com>
David Cooper <david@dtcooper.com>
dodgepong <ben.torell@gmail.com> <dodgepong@gmail.com>
dodgepong <ben.torell@gmail.com>
Robin Hielscher <git@robinhielscher.de> <git@jack0r.com>
Robin Hielscher <git@robinhielscher.de>
stump <git@stump.io> <stump@stump.io>
stump <git@stump.io>
Gary Kramlich <gary@bitmex.com> <grim@reaperworld.com>
Gary Kramlich <gary@bitmex.com>
Joshua Rowe <josh@nocoded.com> <joshua@nvisionweb.com>
Joshua Rowe <josh@nocoded.com>
Matthew McNamara <matt@mattmcn.com> <mattmcnam@gmail.com>
Matthew McNamara <matt@mattmcn.com>
cg2121 <claytong1214@gmail.com>
Gol-D-Ace <Gol-D-Ace@users.noreply.github.com>
JohannMG <johannmg@fb.com>
sorayuki <sorayuki@live.com>
Dead133 <Dead133@gmail.com>
Ilya Melamed <ilya77@gmail.com>
Michel <michel@sokar.nl>
Warchamp7 <warchamp2003@hotmail.com>
bl <bl4@postpile.org>
Palakis <contact@slepin.fr>
paibox <beeanyew@gmail.com>
boombatower <jimmy@boombatower.com>
cristisilaghi <cristian.silaghi@mozilla.ro>
notmark <mark@vaughnsoft.com>
univrsal <univrsal@protonmail.ch>
Farzad Karkhani <farzad.karkhani@gmail.com>
MedicMomcilo <medicmomcilo@gmail.com>
#
# Manual changes made:
#
# <matt@wizardcm.com> -> <matt@obsproject.com>
# Colin Edwards -> DDRBoxman
# Richard Stanway -> R1CH
# Matt Gajownik -> WizardCM
# pkviet -> pkv
# Ben Torrel -> dodgepong
# Clayton Groeneveld -> cg2121
# Andrew Surzhynskyi -> Dead133

23
obs-studio/.swift-format Normal file
View File

@ -0,0 +1,23 @@
{
"version": 1,
"lineLength": 120,
"indentation": {
"spaces": 4
},
"tabWidth": 4,
"maximumBlankLines": 1,
"respectsExistingLineBreaks": true,
"lineBreakBeforeControlFlowKeywords": false,
"lineBreakBeforeEachArgument": false,
"lineBreakBeforeEachGenericRequirement": false,
"lineBreakBetweenDeclarationAttributes": false,
"prioritizeKeepingFunctionOutputTogether": false,
"indentConditionalCompilationBlocks": true,
"lineBreakAroundMultilineExpressionChainComponents": false,
"fileScopedDeclarationPrivacy": {"accessLevel": "private"},
"indentSwitchCaseLabels": false,
"spacesAroundRangeFormationOperators": false,
"noAssignmentInExpressions": { "allowedFunctions" : ["XCTAssertNoThrow"] },
"multiElementCollectionTrailingCommas": true,
"indentBlankLines": false,
}

2894
obs-studio/AUTHORS Normal file

File diff suppressed because it is too large Load Diff

37
obs-studio/CMakeLists.txt Normal file
View File

@ -0,0 +1,37 @@
cmake_minimum_required(VERSION 3.28...3.30)
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/common/bootstrap.cmake" NO_POLICY_SCOPE)
project(obs-studio VERSION ${OBS_VERSION_CANONICAL})
if(CMAKE_HOST_SYSTEM_NAME MATCHES "Windows")
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/windows/architecture.cmake")
if(NOT OBS_PARENT_ARCHITECTURE STREQUAL CMAKE_VS_PLATFORM_NAME)
return()
endif()
endif()
include(compilerconfig)
include(defaults)
include(helpers)
option(ENABLE_FRONTEND "Enable building with UI (requires Qt)" ON)
option(ENABLE_SCRIPTING "Enable scripting support" ON)
option(ENABLE_HEVC "Enable HEVC encoders" ON)
add_subdirectory(libobs)
if(OS_WINDOWS)
add_subdirectory(libobs-d3d11)
add_subdirectory(libobs-winrt)
endif()
add_subdirectory(libobs-opengl)
if(OS_MACOS)
add_subdirectory(libobs-metal)
endif()
add_subdirectory(plugins)
add_subdirectory(test/test-input)
add_subdirectory(frontend)
message_configuration()

View File

@ -0,0 +1,233 @@
{
"version": 8,
"cmakeMinimumRequired": {
"major": 3,
"minor": 28,
"patch": 0
},
"configurePresets": [
{
"name": "environmentVars",
"hidden": true,
"cacheVariables": {
"RESTREAM_CLIENTID": {"type": "STRING", "value": "$penv{RESTREAM_CLIENTID}"},
"RESTREAM_HASH": {"type": "STRING", "value": "$penv{RESTREAM_HASH}"},
"TWITCH_CLIENTID": {"type": "STRING", "value": "$penv{TWITCH_CLIENTID}"},
"TWITCH_HASH": {"type": "STRING", "value": "$penv{TWITCH_HASH}"},
"YOUTUBE_CLIENTID": {"type": "STRING", "value": "$penv{YOUTUBE_CLIENTID}"},
"YOUTUBE_CLIENTID_HASH": {"type": "STRING", "value": "$penv{YOUTUBE_CLIENTID_HASH}"},
"YOUTUBE_SECRET": {"type": "STRING", "value": "$penv{YOUTUBE_SECRET}"},
"YOUTUBE_SECRET_HASH": {"type": "STRING", "value": "$penv{YOUTUBE_SECRET_HASH}"}
}
},
{
"name": "dependencies",
"hidden": true,
"vendor": {
"obsproject.com/obs-studio": {
"dependencies": {
"prebuilt": {
"version": "2026-08-26",
"baseUrl": "https://github.com/obsproject/obs-deps/releases/download",
"label": "Pre-Built obs-deps",
"hashes": {
"macos-universal": "166560b13be8deb9a1aa7bfd306377a92c3817cecace931fe9732b9a25f0a97a",
"windows-x64": "eb5e67ad110543bcb65a4612bcd654b6427972ea70d26412b48d7e21532dfe67",
"windows-x86": "dc21eb9fbc5c08ea0451950e35eeeeece7537e13d1ec7115803b1b59ea96f9b2",
"windows-arm64": "0648c0fd0887efcb718e437bf9c446b069a50be58e7fcd585497ad8c11644a79"
}
},
"qt6": {
"version": "2026-08-26",
"baseUrl": "https://github.com/obsproject/obs-deps/releases/download",
"label": "Pre-Built Qt6",
"hashes": {
"macos-universal": "1f69dcfcdd71f6e319d89736de0290a366fe5d161eae692c6818d20fdfa95c25",
"windows-x64": "7dd60d2883207ecccceb2270538140fccd1b5022e06beedfbc3ba46c7d2e76ae",
"windows-arm64": "a4872414b91252b1d51a3a6d98b6bdd7e3468db19a42c42978670f84eddcb888"
},
"debugSymbols": {
"windows-x64": "c69ad24a76c7986aabff1f0e3860f4e650843dcbbcb2e6699daf63914fa888cd",
"windows-arm64": "e39c0ecfdb5fb19a53623aa4914c07e02989f0320e828cc7ed7c18be6b734cc9"
}
},
"cef": {
"version": "6533",
"baseUrl": "https://cdn-fastly.obsproject.com/downloads",
"label": "Chromium Embedded Framework",
"hashes": {
"macos-x86_64": "37bf7571a48c5dfa8519817e4a90a3503a0eb30f9eadd68f4c3e783e363f272a",
"macos-arm64": "429b50e74f6c174dcfe2f14d8204b54add497eaafe117f7b69ce6bb2354d2626",
"ubuntu-x86_64": "7963335519a19ccdc5233f7334c5ab023026e2f3e9a0cc417007c09d86608146",
"ubuntu-aarch64": "642514469eaa29a5c887891084d2e73f7dc2d7405f7dfa7726b2dbc24b309999",
"windows-x64": "922efbda1f2f8be9e5b2754d878a14d90afc81f04e94fc9101a7513e2b5cecc1",
"windows-arm64": "df9df4bd85826b4c071c6db404fd59cf93efd9c58ec3ab64e204466ae19bb02a"
},
"revision": {
"macos-x86_64": 5,
"macos-arm64": 5,
"ubuntu-x86_64": 6,
"ubuntu-aarch64": 6,
"windows-x64": 2
}
}
},
"tools": {
"sparkle": {
"version": "2.9.2",
"baseUrl": "https://github.com/sparkle-project/Sparkle/releases/download",
"label": "Sparkle 2",
"hash": "1cb340cbbef04c6c0d162078610c25e2221031d794a3449d89f2f56f4df77c95"
}
}
}
}
},
{
"name": "macos",
"displayName": "macOS",
"description": "Default macOS build (single architecture only)",
"inherits": ["environmentVars"],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
},
"generator": "Xcode",
"binaryDir": "${sourceDir}/build_macos",
"cacheVariables": {
"CMAKE_OSX_DEPLOYMENT_TARGET": {"type": "STRING", "value": "13.0"},
"OBS_CODESIGN_IDENTITY": {"type": "STRING", "value": "$penv{CODESIGN_IDENT}"},
"OBS_CODESIGN_TEAM": {"type": "STRING", "value": "$penv{CODESIGN_TEAM}"},
"OBS_PROVISIONING_PROFILE": {"type": "STRING", "value": "$penv{PROVISIONING_PROFILE}"},
"VIRTUALCAM_DEVICE_UUID": {"type": "STRING", "value": "7626645E-4425-469E-9D8B-97E0FA59AC75"},
"VIRTUALCAM_SINK_UUID": {"type": "STRING", "value": "A3F16177-7044-4DD8-B900-72E2419F7A9A"},
"VIRTUALCAM_SOURCE_UUID": {"type": "STRING", "value": "A8D7B8AA-65AD-4D21-9C42-66480DBFA8E1"},
"SPARKLE_APPCAST_URL": {"type": "STRING", "value": "https://obsproject.com/osx_update/updates_$(ARCHS)_v2.xml"},
"SPARKLE_PUBLIC_KEY": {"type": "STRING", "value": "HQ5/Ba9VHOuEWaM0jtVjZzgHKFJX9YTl+HNVpgNF0iM="},
"ENABLE_BROWSER": true
}
},
{
"name": "macos-ci",
"displayName": "macOS (CI)",
"description": "CI macOS build (single architecture only)",
"inherits": ["macos"],
"warnings": {"dev": true, "deprecated": true},
"cacheVariables": {
"CMAKE_COMPILE_WARNING_AS_ERROR": true,
"CMAKE_XCODE_ATTRIBUTE_COMPILATION_CACHE_ENABLE_CACHING": "YES",
"CMAKE_XCODE_ATTRIBUTE_COMPILATION_CACHE_CAS_PATH": "$penv{XCODE_CAS_PATH}"
}
},
{
"name": "ubuntu",
"displayName": "Ubuntu",
"description": "obs-studio for Ubuntu",
"inherits": ["environmentVars"],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
},
"binaryDir": "${sourceDir}/build_ubuntu",
"generator": "Ninja",
"warnings": {"dev": true, "deprecated": true},
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_INSTALL_LIBDIR": "lib/CMAKE_SYSTEM_PROCESSOR-linux-gnu",
"ENABLE_AJA": false,
"ENABLE_VLC": true,
"ENABLE_WAYLAND": true,
"ENABLE_WEBRTC": false
}
},
{
"name": "ubuntu-ci",
"inherits": ["ubuntu"],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
"CMAKE_COMPILE_WARNING_AS_ERROR": true,
"CMAKE_COLOR_DIAGNOSTICS": true,
"ENABLE_CCACHE": true
}
},
{
"name": "windows-x64",
"displayName": "Windows x64",
"description": "Default Windows build (x64)",
"inherits": ["environmentVars"],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"architecture": "x64,version=10.0.26100.0",
"binaryDir": "${sourceDir}/build_x64",
"generator": "Visual Studio 18 2026",
"cacheVariables": {
"GPU_PRIORITY_VAL": {"type": "STRING", "value": "$penv{GPU_PRIORITY_VAL}"},
"VIRTUALCAM_GUID": {"type": "STRING", "value": "A3FCE0F5-3493-419F-958A-ABA1250EC20B"},
"ENABLE_BROWSER": true
}
},
{
"name": "windows-ci-x64",
"displayName": "Windows x64 (CI)",
"description": "CI Windows build (x64)",
"inherits": ["windows-x64"],
"warnings": {"dev": true, "deprecated": true},
"cacheVariables": {
"CMAKE_COMPILE_WARNING_AS_ERROR": true
}
},
{
"name": "windows-arm64",
"displayName": "Windows ARM64",
"description": "Default Windows build (ARM64)",
"inherits": ["environmentVars"],
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"architecture": "ARM64,version=10.0.26100.0",
"binaryDir": "${sourceDir}/build_arm64",
"generator": "Visual Studio 18 2026",
"cacheVariables": {
"GPU_PRIORITY_VAL": {"type": "STRING", "value": "$penv{GPU_PRIORITY_VAL}"},
"VIRTUALCAM_GUID": {"type": "STRING", "value": "A3FCE0F5-3493-419F-958A-ABA1250EC20B"},
"ENABLE_AJA": false,
"ENABLE_BROWSER": true,
"ENABLE_SCRIPTING": false,
"ENABLE_VST": false
}
},
{
"name": "windows-ci-arm64",
"displayName": "Windows ARM64 (CI)",
"description": "CI Windows build (ARM64)",
"inherits": ["windows-arm64"],
"warnings": {"dev": true, "deprecated": true},
"cacheVariables": {
"CMAKE_COMPILE_WARNING_AS_ERROR": true
}
}
],
"buildPresets": [
{
"name": "windows-x64",
"configurePreset": "windows-x64",
"displayName": "Windows 64-bit",
"description": "Windows build for 64-bit (aka x64)",
"configuration": "RelWithDebInfo"
},
{
"name": "windows-arm64",
"configurePreset": "windows-arm64",
"displayName": "Windows on ARM 64-bit",
"description": "Windows build for ARM 64-bit (aka ARM64)",
"configuration": "RelWithDebInfo"
}
]
}

153
obs-studio/COC.rst Normal file
View File

@ -0,0 +1,153 @@
Code of Conduct
===============
Preamble
--------
OBS is made up of many people from all over the world, with different
backgrounds and experiences that they bring to the project. In order to
continue letting the community grow, feel safe, and to encourage good
communication, we are adopting the following Code of Conduct. This
document—or any subsequent version—will be enforced from July 31st, 2021
onwards.
Scope
-----
This Code of Conduct applies to anyone who contributes—or wishes to
contribute—to the project directly, is taking part in community
discussion in official channels, or are in a position where they’ll be
seen as a representative of the project, regardless of whether they
intend to speak for the project or not.
A person is considered to be representing the project if they’re
speaking as a code contributor, support volunteer, member involved with
the project, or if they have some kind of visible role in an official
channel for OBS. This does not include roles for Sponsors or Patrons.
Official channels for OBS include the Discord server, the forum, any
GitHub repositories within the OBS Project organization, any social
media accounts for OBS Project, any public events where OBS Project has
a presence, any presentations given on behalf of OBS Project, or any
other situation that can be reasonably interpreted as being hosted,
attended or communicated by the OBS Project.
This Code of Conduct is subject to change, and any changes will have a 2
week notice period before taking immediate effect. Changes will not be
enforced retroactively unless it directly affects the security, safety
or well-being of another individual, or other exceptional circumstances.
Guidelines
----------
General
~~~~~~~
- We are committed to providing a friendly, safe and welcoming
environment for all, regardless of level of experience, gender
identity and expression, sexual orientation, disability, personal
appearance, body size, race, ethnicity, age, religion, nationality,
or other similar characteristic.
- Please avoid using overtly sexual aliases or other nicknames that
might detract from a friendly, safe and welcoming environment for
all.
- Please be kind and courteous. There’s no need to be mean or rude.
- Respect that people will have differences of opinion.
- We will exclude you from interaction if you insult, demean or harass
anyone. That is not welcome behavior. The definition of harassment is
provided in the section on Unacceptable Behavior. In particular, we
don’t tolerate behavior that excludes people in socially marginalized
groups. This includes private harassment.
- Likewise any spamming, trolling, flaming, baiting or other
attention-stealing behavior is not welcome.
- Being a recognized Contributor, Support Volunteer, Sponsor or Patron
does not give you special right to avoid following these guidelines
when taking part in community discussions.
Code Contributors
~~~~~~~~~~~~~~~~~
- Respect that every design or implementation choice carries a
trade-off and numerous costs. There is seldom a right answer.
- Please keep unstructured critique to a minimum. If you have solid
ideas you want to experiment with, make a fork and see how it works.
- Do not post negative or inflammatory comments on code submissions. If
you believe that a code submission has problems, provide constructive
or actionable feedback, or a reason for why a submission is being
rejected. Where it is possible, provide explanations that someone can
use to fix things.
Support Volunteers
~~~~~~~~~~~~~~~~~~
- There are many users of differing skill levels or abilities. This is
never a reason to refuse support for someone, as long as it’s within
the scope of OBS support, and the user is willing to cooperate.
- Just because something seems obvious to you, doesn’t mean it’s
actually obvious. If someone is having difficulty understanding how
to use OBS, it should never be treated as their fault.
- Bad advice is often worse than no advice. If you don’t know how to
address someone’s issue, then get ahold of someone who’ll be able to
provide them help. We can only expect so much patience from a user
asking for support, and helping without understanding the problem
only results in using up that patience.
Unacceptable Behavior
---------------------
The following behaviors are considered harassment and are unacceptable
within our community:
- Violence, threats of violence or violent language directed against
another person.
- Sexist, racist, homophobic, transphobic, ableist or otherwise
discriminatory jokes and language.
- Posting or displaying sexually explicit or violent material.
- Posting or threatening to post other people’s personally identifying
information ("doxing").
- Personal insults.
- Unwelcome sexual attention. This includes, sexualized comments or
jokes, and unwelcome sexual advances.
- Deliberate intimidation, stalking or following.
- Advocating for, or encouraging, any of the above behavior.
Enforcement
-----------
Instances of unacceptable behavior may be reported by contacting an
enforcement team member at one of the following addresses:
coc@obsproject.com. All complaints will be reviewed and investigated,
and will result in a response that is deemed necessary and appropriate
to the circumstances. The project team is obligated to maintain
confidentiality with regard to the reporter of an incident. Further
details of specific enforcement policies may be posted separately.
In other cases where behavior is against the general guidelines set out
in this Code of Conduct, this may be brought up with a moderator through
either a Discord DM, or through an email to an enforcement member. In
these cases, it is always assumed in the first instance that a person
means well, and action will only be taken in the case of repeated
failure to follow the guidelines.
Feedback
--------
Suggestions for changes can be made by submitting a pull request on the
`obsproject/obs-studio <https://github.com/obsproject/obs-studio>`__
repo, or if you wish to privately discuss your concerns, you can instead
email coc@obsproject.com.
Attribution
-----------
This Code of Conduct is adapted in part from the Contributor Covenant,
version 1.4, in part from the Citizen Code of Conduct (used under
CC-BY-SA 3.0), and in part from the Rust Code of Conduct.
This Code of Conduct is released and licensed under a Creative Commons
Attribution-ShareAlike 3.0 License.
Version History
---------------
V1.0 - First Iteration - July 31st, 2021

425
obs-studio/CODESTYLE.md Normal file
View File

@ -0,0 +1,425 @@
OBS Studio Code Style Guidelines
================================
The project requires all contributions to have their source code formatted using appropriate formatting tools to reduce the potential impact of stylistic changes to a structured “diff” view of code.
In addition to those automatically enforceable stylistic choices the project also prefers contributions to follow a set of architectural guidelines that are chosen to reduce undefined or unexpected behavior and make code easier to reason about during reviews or maintenance.
## Reducing Potential For Errors
The following guidelines have been established as good practices to reduce some common potential for errors or naive coding practices that have lead to such errors in the past:
* **Always Initialize Variables** - Depending on the programming language, language standard, compiler, and platform, rules for when and how variables are initialized automatically might differ. Thus some variables might have random values at program start and naive code might interpret any value but “0” to mean “correctly initialized” and run into unexpected behavior.
* Do not declare or initialize multiple variables on the same line, do not mix declarations and initializations
```C
int i, v = 0; // BAD - v is initialized to 0, i is uninitialized
int i = 0; // GOOD - i is explicitly declared and initialized
int v = 0; // GOOD - v is separately declared and initialized
```
* **Do not use “0” as a valid enumeration value by default** - in many cases an enum requires an explicit choice to be made (only one value of a set of “valid” values can be set) and “not making a choice” should not be considered valid
```C
enum state { ACTIVE, // BAD - zero-initialized enum potentially
INACTIVE, // leads to implicit state changes.
DELETED
};
enum state { INVALID, // GOOD - zero-initialized enum produces
ACTIVE, // an invalid value by default, avoiding
INACTIVE, // an implicit state change.
DELETED
};
enum state { ACTIVE = 1, // GOOD, zero-initialization fails because
INACTIVE = 2, // it’s not a valid enum value to begin with.
DELETED = 3
};
```
* **Use natural language for variables and types** - expressive code is easier to reason about for maintainers and reviewers and also reduces the mental burden when returning to the same code after even a short absence, as code “says what it does” and variables “tell what they represent”.
```C
int c = 1; // BAD: What does “c” represent?
int count = 2; // BAD: Count of “what”?
int num_bytes = 3; // GOOD: “Num(ber) of bytes”
bool valid; // BAD: Meaning is ambiguous
bool has_valid_key; // GOOD: Describes state of element in an object
bool is_valid; // GOOD: Describes state of element itself
bool did_send_packet; // GOOD: Describes state of transaction.
float dur = 1.0; // BAD: Unit of duration unknown
float duration_ms = 5.0; // GOOD: Unit of duration encoded within name
// GOOD: Function signature communicates the unit of the delay explicitly.
start_transition_with_delay(transition_type *transition, float delay_ms);
```
* **Use compound types rather than individual variables** - information like the size or dimension of an object, a timescale, a position in space, should be logically encoded in a compound type and only “unwrapped” when consumed
```C
// GOOD: Function signature communicates the unit of the delay explicitly.
start_transition_with_delay(transition_type *transition, float delay_ms);
// EVEN BETTER: Time values encoded as pieces of a fraction (1/1000th of a
// second representing a “microsecond”) and explicitly passed to
// the function.
typedef struct {
int_64_t time_value;
int_32_t time_scale;
} time_unit;
start_transition_with_delay(transition_type *transition, time_unit delay);
// BAD: Behavior encoded in unrelated booleans, maybe even conflicting
start_transition(transition_type *transition, bool ignore, bool abort);
// GOOD: Function signature requires more meaningful enum rather than bool
enum transition_abort_mode {
TRANSITION_ABORT_INVALID, TRANSITION_ABORT_ALL,
TRANSITION_ABORT_NEW, TRANSITION_ABORT_NONE
};
// Signature:
start_transition(transition_type *transition, enum transition_abort_mode);
```
* **Do not use unnecessary abbreviations** - if a class implements an "Advanced Output", call it AdvancedOutput, not AdvOutput. The same applies to variables holding an instance of the class. Code is read more often than it is written (and indeed the writing part is just the final outcome of a much longer reasoning process) and optimising for the latter incurs a debt on the former.
* **Name functions after what they do rather than how they do it** - a function’s signature is effectively its “interface” and should provide a hint about which functionality it provides to the caller. The actual implementation should not be relevant to the caller and exposing this information can actually have a net-negative effect as the caller might try to “out-think” the API.
* **Prefer pure functions and non-mutable variables** - side-effects are harder to keep track of and can lead to confusing results, particularly when following the prior rule. This also ties into a later rule about avoiding global scope, as global (and shared) variables tend to encourage writing code that surreptitiously changes global shared scope, which makes code harder to test and reason about.
> [!NOTE]
> This obviously does not apply to methods where the implementation detail is a technical need of the API user e.g., for factory methods where the user has data in a specific place that it needs to be loaded from, such as `MyData::loadFromUrl(const std::string &url)` or `MyData::loadFromFile(const std::filesystem::path &path)` where the “what” is essentially intertwined with the “how”.
* **Do not use specific integer types outside of binary protocols** - for the vast majority of cases using an “int” is sufficient and using a signed type also prevents unexpected overflow issues where a small negative number becomes a massive positive number. Use sized types (and unsigned types) when interacting with binary protocols, bitfields (e.g. to explicitly remove any special meaning from the most-significant bit). Prefer 64-bit integers over unsigned 32-bit integers for large numbers.
* **Do not micro-optimize and do not attempt to be overly "clever" with an implementation** - to quote Kernighan:
> Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?
* **Use profiling tools to identify actual opportunities for optimization** - the code that the compiler generates can potentially work in different ways than the source code might suggest and might not even be executed in the same order. Premature optimizations might also prevent the compiler from applying its own, leading to slower code overall. Also be mindful of profiling tools themselves potentially influencing the runtime behaviour of code.
* **Attempt to actively break code during testing** - do not rely on “happy paths”. Try to be creative and come up with ways to break the assumptions your code might have made. Do not rely on assumptions that data will never be “wrong” or that some other data will always be “there”. As the ISO C++ FAQ calls it:
> Write code that is guaranteed to work, not code that doesn’t seem to break.
* **Write code that will be reasonably easy to understand even 6, 12, or 24 months later** - while it is easy to reason about the state of a program while working on it, much of that “inside knowledge” might be gone even a few weeks later. Writing code that is obvious in what it does (and also why it does certain things and which pieces of data it requires) will make it easier to get back into the mental model behind the code. **Write code so that any newcomer to the project will have a reasonably easy time understanding what it does and why it does it the way it’s implemented.**
* **Do not rely on global state or singletons** - both are easily abused as shortcuts to avoid implementing a more expressive and safer design. There are valid use cases for global state (like a global “application” instance implemented as a singleton) but those are the exception and not the rule.
* **Treat programming as a mental exercise and not just as text manipulation** - existing code in particular will have been designed and built with a set of assumptions and theories in mind, which themselves informed the architectural choices made in its implementation. Some changes might seem “quick and easy” but <u>potentially violate those architectural assumptions</u> and thus could leave the code in a precarious state (and indeed might lead to undefined behaviour when yet other code relies on these assumptions not to be violated).
The last point cannot be overstated because it is a common source of friction in long-lived software projects. **Neither source code nor documentation can be a full representation of the “theory” behind the code** and indeed both can never be more than an incomplete “snapshot” of one possible implementation of this theory.
But without a decent understanding of the theory behind the code (or the architectural concerns that went into the current design) one cannot correctly ascertain which changes will be “in line” with the way current code works and might instead violate important principles of it.
And the more such “naive” code (mainly in the form of hacks and workarounds) is piled on, the more difficult the code becomes to work with, culminating in a code base where even shipping “simple” new features becomes a tough exercise (as the code starts to behave in unpredictable ways due to all the violations of the original “theory”).
> [!NOTE]
> This principle becomes more obvious with less abstract examples: An image-editing program will have a set of considerations and constraints that went into its design (code and user interface) and just adding the ability to decode video files will not make the program able to edit video, which is an entirely separate discipline with potentially opposing needs and considerations.
>
> And indeed any new feature added to the photo editor now has to potentially contend with the reality that it might be faced with a video file instead, adding even more bits and pieces to unrelated parts of the program to handle a scenario the foundation of the program was conceptually (and explicitly) not built for.
## Language Specific Guidance
OBS Studio currently contains code written in the following languages:
* C
* C++
* Objective-C/C++
* Swift
* CMake
Continuous integration code is mostly based on Powershell, Zsh or Bash scripts, and Python 3.
While C++ and ObjectiveC/C++ are supersets of the C language, the project prefers to treat them individually with their own rules, conventions, and language standards. Thus rules that apply to “C” do not necessarily apply to these languages and indeed some rules will be replaced for those languages.
### C
For “pure” C code the project follows the Linux Kernel Coding Style (https://github.com/torvalds/linux/blob/master/Documentation/process/coding-style.rst). Parts of the guidelines that relate to Emacs, kernel-level allocators, or macros only available in the Linux kernel source code do not apply.
> [!IMPORTANT]
> The current C language standard of the project is **C17 without GNU extensions**.
Some additional notes:
* Formatting is checked and can be applied by `clang-format`. The formatting generated by it supersedes any rules in the guidelines.
* The project uses a line length of 120 characters, and uses tabs for indentation with a tab width of 8 characters.
### C++
The project treats C++ as its own language and not just as “C with classes”. The associated code style guidelines are based on the Google C++ Code Style Guide (https://google.github.io/styleguide/cppguide.html) with changes to some aspects, as listed below.
> [!IMPORTANT]
> The current C++ language standard of the project is **C++17 without GNU extensions**. A move to C++20 is currently being considered.
Additions and changes (in the order the associated topics appear in the Google document):
* **Header Files**
* OBS Studio uses the “cpp” suffix for C++ source code files rather than “cc” and “hpp” for C++-specific header files
* Header guards are permitted, but not required - a simple `#pragma once` is sufficient
* Use of forward declarations is permitted for class or struct types, but be mindful of the caveats mentioned in the Style Guide. **Avoid “Structuring code to enable forward declarations”.**
* OBS Studio uses a different header include order, documented in a separate section below
* **Classes**
* If any constructor or assignment operator is defined, all 5 types need to be either defined, defaulted, or deleted, including the destructor (**“Rule of Five”**)
* Ensure that move constructor and move assignment operator are marked “noexcept” (and implemented accordingly) to avoid possible pessimization when using user-defined types with standard library containers like `std::vector`.
* Use multiple-inheritance only for the interface/protocol pattern ("implements an interface as defined by" relationship), avoid the diamond pattern and virtual base classes.
* **Google-Specific Magic**
* OBS Studio does not use `cpplint`
* **Other C++ Features**
* Exceptions can (but don't have to) be used, particularly when no good "sentinel value" can be provided and also to avoid littering the code with correctness checks after every function call. It is preferred to keep the “scope” of exceptions within the same module (i.e., the same executable or library).
* Be aware that in C++ <u>any</u> function that is not a destructor and is not explicitly marked as noexcept can potentially throw an exception as part of normal error handling rather than signaling an actual critical failure.
* Instance methods that modify any data, even if it is not their own instance's data, should not be marked `const` to signal that they <u>logically</u> mutate some state.
* Do not use comments to declare the types in structured bindings
* Any guidelines of C++20 features will be evaluated once the project switches to that language standard.
* The project does not allow the use of the Boost library.
* However the project does allow the use of the “Disallowed standard library features” and some “Nonstandard Extensions”.
* **Naming**
* Use the class name (following type name rules) for the file name of its interface header and implementation file.
* Use `camelCase` for instance methods, functions, as well as variables. This also aligns with Qt’s code style.
* Existing constants in code are permitted to be kept with `UPPERCASE` names, but new constants should follow the new convention of `kCamelCase`.
* Namespaces need to follow type name rules, though the project only uses the “`OBS`” namespace for refactoring of application code for the time being.
* Do not use underscore prefixes for any names, as these are reserved for the standard library implementation. Use a trailing underscore for private member variables.
* **Comments**
* Only C++ comment style is permitted for C++ source code, mixing of different styles is not allowed.
* Do not use function argument comments.
* **Formatting**
* Formatting is checked and can be applied by `clang-format`. The formatting generated by it supersedes any rules in the guidelines.
* The project uses a line length of 120 characters, and uses tabs for indentation with a tab width of 8 characters.
* All new C++ code needs to use curly braces for all looping or branching statements. No exceptions.
* Prefer brace initialization to protect against (unexpected) narrowing and prefer it over C-style assignment, except where a functional difference in behavior is needed (e.g. for `std::vector`)
* Class visibility labels are not indented.
Some additional notes:
* Use the C++ standard template library and C++ algorithms as much as possible, but also be aware of known issues with some of them (e.g. bad runtime performance of `std::regex`).
* **Prefer the standard library** over implementing custom loops.
* Prefer **C++ collections and filesystem functions** over their C variants (e.g. `std::array` over C arrays)
* Prefer **range-based and iterator-based loops** over counter-based loops.
* **Prefer C++ types over C types**
* Wrap C library code in C++ code to provide a clean C++ interface first.
* **Use `class enum` instead of `enum`** for enumeration values and do not use enum as “integer” value aliases.
* Do not use inline or static in the same way they are used in C
> [!IMPORTANT]
> The “`static`” keyword is one of the more confusing aspects of modern C++, particularly compared to C. The meaning of the keyword changes depending on whether it is used for a function, a global variable, a function-scope variable, a class method, or a class member.
>
> In general limit its use in C++ code to describe “storage duration” of variables or for class methods (e.g. factory methods). Be mindful that even though since C++11 function-local static variables are initialized non-concurrently, this does not make consecutive _access_ of the object thread-safe as well. Also be mindful of the “Static Initialization Order Fiasco”. Otherwise all the common established pitfalls and issues of global variables apply.
>
> `constexpr` definitions do not need to use the static keyword. `constexpr` implies const and const implies static storage duration by default.
>
> Be careful when mixing `static` and `inline`, as the inline keyword (short for “defined in-line”) allows the same definition to exist in multiple translation units (and thus is allowed to violate the one-definition rule, or “ODR”), enabling the linker to deduplicate all instances of the same function, which is the exact opposite of what “`static`” requires (local visibility and thus a distinct copy of the function in each translation unit).
#### The Exception: Qt Guidelines
Many of Qt’s core concepts were invented years before C++ added support for similar ideas, which means that much code interacting with Qt library functions and QObject-based instances requires violating some of the core language principles outlined above:
* Qt's ownership model predates modern smart pointers or references and thus requires passing around raw pointers:
* Any class derived from QWidget needs to be instantiated using bare new.
* Parent QObjects take ownership of children and will take handle destructing them appropriately
* Thus: **Do not call `delete` on a widget owned by a parent widget. Do not create widgets without attaching them to a parent widget.**
* Qt's default string class QString was an early adopter of Unicode (like Windows or Cocoa on macOS) and uses a 2-byte encoding internally (which was adapted to become UTF-16)
* A QString needs to be converted between UTF-16 and UTF-8 when passing character data to any C-based API or when interacting with `std::string` instances.
* **There Ain’t No Such Thing As Plain Text** (https://tonsky.me/blog/unicode/)
* The lifetime of a heap object passed to Qt must be guaranteed to match or exceed the lifetime of the using/owning Qt object.
* Lambda expressions can and should be used for callback-style programming as much as possible.
* **Be aware of lifetime issues however**: Any reference captured by a lambda needs to be "alive" for as long as Qt might potentially invoke the lambda expression. Use copy-based capture for scalar values.
* Note that a pointer is "just" a scalar value and is thus captured by copy, but there is no strong relationship between the copied pointer and the memory it is pointing to. The lifetime of the heap object needs to be ensured. Be mindful of the lifetime of whomever will be invoking the lambda.
* A pointer captured by reference can potentially be set to nullptr and checked within the lambda expression's body, but the lifetime of the pointer reference itself now needs to be ensured.
### C/C++ Header Include Order
Prefer to include the headers that the implementation itself needs (e.g. because a type or definition is used directly in the implementation) and do not rely on another header possibly having included the same file already. **This follows the “include what you use” rule**.
When including headers, use the following order:
```C++
// Interface definition or “counterpart” of current file
#include “interface.h”
// File in the same directory as the current file _if_ header belongs to the
// same “implementation”.
#include “file_in_working_directory.h”
// First party dependency from the same larger project that is “linked” with
// the implementation
#include <first_party_dependency/type_or_interface.h>
// Third party dependency not part of the same project and “linked” with the
// implementation.
#include <third_party_dependency/type_or_interface.h>
// C++ standard library includes
#include <string>
// C standard library includes
#include <sys/socket.h>
```
> [!NOTE]
> This specific order of includes helps in identifying potentially “broken” headers without “masking” the issue by including potential dependencies first.
>
> That issue can be avoided by following a simple procedure when creating a new interface and implementation pair: The initial implementation should always include the “counterpart” interface header first and should compile just fine even with the “empty” implementation.
>
> The same applies to any first party and third party library headers: Including one by itself should not result in compilation issues or should not require any standard library headers being included first (if that’s the case, the corresponding library header is badly designed).
>
> Putting the standard library includes last (and only including them if the implementation needs them) helps expose such malformed header files. While this scheme relies on convention (rather than enforcement by the language) it leads to self-contained headers and a cleaner set of includes.
Additional rules for this guideline:
* Includes within each block need to be sorted alphabetically with case-sensitivity enabled.
* Do not create platform specific “include blocks”. If a single source file has to include a hodgepodge of platform-specific headers it usually suggests that the source file tries to do “too much” and the “uglyness” that the rule might lead to is the point. To quote the Google C++ Style Guide:<br ><br />**“Instead of using a macro to conditionally compile code ... well, don't do that at all”.**<br /><br />
* Refactor the source file to be platform-specific and use CMake to add the appropriate file to the target for a specific platform.
* Do not use double quotes to include any file not in the same working directory as the current source or header file. Compilers have been lenient and automatically attempt to use header include paths for all header files, so files might indeed compile nonetheless, but the additional context for the reader (“that header is provided by some other module/target in the project”) is lost.
Some header files will require breaking these rules. Well-known examples include:
* `windows.h` might be required very early in an implementation, particularly when relying on the magic `WIN32_LEAN_AND_MEAN` define, as nested includes of the same header might break compilation in severe ways otherwise..
* The BSD header `libprocstat.h` requires additional headers to be included in a specific order before its own inclusion (as documented in its man page).
* If an impact of include order can be proven through testing, additional headers are allowed to break these rules. This is unavoidable given the legacy and preprocessor architecture of C and C++.
### Objective-C/C++
For Objective-C/C++ code the Google Objective-C Style Guide (https://google.github.io/styleguide/objcguide.html) should be followed, which itself is based on Apple’s [Cocoa Coding Guidelines](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CodingGuidelines/CodingGuidelines.html) and [Programming with ObjC Conventions](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Conventions/Conventions.html).
> [!IMPORTANT]
> The current Objective-C/C++ language standard of the project is **Objective-C 2.0**.
The additions and changes to the Google Style Guide are as follows (in the order the associated topics appear in the Google document):
* **Naming**
* Use `camelCase` for functions.
* Use of the `g` prefix for global variables in file scope is permitted.
* **Types**
* Always use the native 64-bit types (i.e., double for CGFloat).
* **Comments**
* Use Apple’s DocC format for documentation (https://www.swift.org/documentation/docc/).
* Use C++ comment style for inline comments.
* **Cocoa and Objective-C Features**
* Use `#import` for all includes, follow the include order as specified for C/C++.
* **Spacing and Formatting**
* Formatting is checked and can be applied by `clang-format`. The formatting generated by it supersedes any rules in the guidelines.
* Use 4 spaces for indentation, use spaces for alignment.
* The maximum line length is 120 characters.
* All code needs to use curly braces for all looping or branching statements. No exceptions.
### Swift
For Swift code the Google Swift Style Guide (https://google.github.io/swift/) should be followed as well as Apple’s Swift API Guidelines (https://www.swift.org/documentation/api-design-guidelines/).
> [!IMPORTANT]
> The current Swift language standard of the project is **Swift 6**.
The additions and changes to the Google Style Guide are as follows (in the order the associated topics appear in the Google document):
* **General Formatting**
* Formatting is checked and can be applied by `swift-format`. The formatting generated by it supersedes any rules in the guidelines.
* Use 4 spaces for indentation, use spaces for alignment.
* The maximum line length is 120 characters.
Some additional notes:
* **Use the Unmanaged type and associated protocols to create retained or unretained opaque pointers to share with C APIs.**
* Use `passRetained` to pass an opaque pointer with an incremented reference count to a C API.
* Use `takeRetained` to take ownership of that reference count in Swift code and allow normal lifetime management to take over.
* Use `passUnretained` and takeUnretained to pass/receive pointers without influencing their reference count.
* **Object lifetime has to be manually ensured, and is thus unmanaged**
* Use extension to implement protocols in code blocks separate from the core type implementation.
```Swift
class MyType {
// Basic implementation
fileprivate let memberVariable: String
init(argumentOne: String) {
self.memberVariable = argumentOne
}
}
extension MyType : SomeProtocol {
func someProtocolMethod(argument: String) -> String {
return “\(memberVariable) \(argument)”
}
}
```
* **Prefer functional patterns as much as possible over C-style loops.**
* When a loop is still preferable, use the native Range type and enumerated loops if a counter value is required:
```Swift
for i in 0..<someValue {
doTheThing()
}
for (i, theThing) in theCollection.enumerated() {
doTheThing(with: theThing);
print("I am on iteration \(i) here...")
}
```
### Objective-C/C++ and Swift
* **Be mindful of reference counts** when sharing Objective-C/C++ or Swift object instances or their data with external code:
* Explicitly increment the reference count on an object to ensure its not deallocated when the current function scope is left and to ensure that the pointer shared with C code is kept alive
* Balance that reference count on an object when the external code calls for its "destruction" by decrementing the reference count - **do not manually deallocate the object**, instead let its reference count go to `0`
* Wrap code that potentially creates a lot of new heap objects in autoreleasepools within a loop body to allow the reference counts to be evaluated early within each iteration
* This can potentially help keeping the memory footprint of each loop iteration at the same level
* Prefer language specific collection and string types over C array or character pointers.
* For Objective-C/C++, prefer `NSString`, `NSDictionary`, `NSArray`, `NSSet`, and others
* For Swift, prefer `String`, `Dictionary`, `Array`, `Set`, and others
* Swift and Cocoa types are bridged and interoperable
* **Prefer Objective-C/C++ blocks and Swift closures over lambda expressions**
### CMake
CMake underwent a big philosophical change with version 3 of the build system generator, which established the notion of “targets” with associated “target properties” that describe a target’s needs, including compiler arguments, linker flags, preprocessor definitions, and more.
The project’s build system was rewritten a few years ago to make full use of these “modern” CMake patterns and approaches, and thus requires all new CMake code to follow the same principles:
* Refer to “Modern CMake” (https://cliutils.gitlab.io/modern-cmake/README.html) for general guidelines about how to write modern CMake code.
* **Always prefer targets** and the use of target properties over magic global variables
* **Prefer generator expressions** over explicit branches in CMake code, if it makes the overall code clearer
* **Use 2 spaces for indentation** and spaces for orientation
* Use `UPPER_SNAKE_CASE` for cache variables, global variables, and file-local variables
* Use `snake_case` for function names and function-local variables
* Use an `_UNDERSCORE_PREFIX` for "private" variables that need to exist in the global or file-local scope
> [!NOTE]
> Macros differ from functions in that they do not have a function-local scope. A macro body is effectively put into the scope it is called from and thus shares the variable scope.
* Pass variables to functions by their name, use `${}` dollar expansion to pass their value
* **Always use double quotes for strings**
* Prefer using functionality either available via CMake itself or available for specific generators over custom code that runs at configure time
* Particularly when multi-config generators for Visual Studio or Xcode are used, code running at configure time does not have access to the "actual" build configuration that will be used by the IDE and thus is incapable of achieving its potential goal anyway.
It is important to remember that CMake does not represent “the build system”, but rather is a “build system generator” that translates the abstract dependency tree of “targets” into an actual build system or IDE project.
### JSON and YAML
Both file formats are predominantly used for build system configuration or continuous integration, but are still subject to a limited set of rules:
* **Use schema files as much as possible** to ensure that a given JSON or YAML file uses correct structure and - as far as possible - valid values.
* Some editors can pull up common schema files when a loaded JSON or YAML file has a specific file name or sits in a directory of a specific name.
* Use `camelCase` for variable names
* **Exception:** Job names for GitHub action workflows commonly use `dash-case` for the job names.
* **Exception:** If the key of a variable is used for an underlying system e.g., to define environment variables in a shell environment that commonly uses `UPPER_SNAKE_CASE` names.
* Use **double quotes** for **key names** and **string values** in JSON files.
* Use **single quotes** for **complex strings in YAML files** that might otherwise not be correctly interpreted as strings, quoting is otherwise not necessary.

46
obs-studio/COMMITMENT Normal file
View File

@ -0,0 +1,46 @@
GPL Cooperation Commitment
Version 1.0
Before filing or continuing to prosecute any legal proceeding or claim
(other than a Defensive Action) arising from termination of a Covered
License, we commit to extend to the person or entity ('you') accused
of violating the Covered License the following provisions regarding
cure and reinstatement, taken from GPL version 3. As used here, the
term 'this License' refers to the specific Covered License being
enforced.
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly
and finally terminates your license, and (b) permanently, if the
copyright holder fails to notify you of the violation by some
reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you
have received notice of violation of this License (for any work)
from that copyright holder, and you cure the violation prior to 30
days after your receipt of the notice.
We intend this Commitment to be irrevocable, and binding and
enforceable against us and assignees of or successors to our
copyrights.
Definitions
'Covered License' means the GNU General Public License, version 2
(GPLv2), the GNU Lesser General Public License, version 2.1
(LGPLv2.1), or the GNU Library General Public License, version 2
(LGPLv2), all as published by the Free Software Foundation.
'Defensive Action' means a legal proceeding or claim that We bring
against you in response to a prior proceeding or claim initiated by
you or your affiliate.
'We' means each contributor to this repository as of the date of
inclusion of this file, including subsidiaries of a corporate
contributor.
This work is available under a Creative Commons Attribution-ShareAlike
4.0 International license (https://creativecommons.org/licenses/by-sa/4.0/).

138
obs-studio/CONTRIBUTING.md Normal file
View File

@ -0,0 +1,138 @@
OBS Studio Contribution Guidelines
============
OBS Studio accepts contributions via public **pull requests**, which encompass changes made to the project as well as a written description of the changes made and the motivation behind them.
Pull requests can be opened by any user with sufficient contribution permissions on the platform chosen by the project. Likewise, any user with sufficient access permissions is able to read, comment, and suggest changes to a pull request, but the ultimate decision for or against adopting the suggested changes lies with **project members**.
OBS Studio has shown steady growth of popularity, complexity, and scope throughout its existence. Contributions that demonstrate they have taken the aspects outlined in this document into account (including the impact to the maintainability of the project) will potentially, but not necessarily, have a higher chance of adoption.
> [!NOTE]
> Past acceptance of a change, including “prior art” that exists in the codebase, does not guarantee that future, similar, changes will be accepted, as the “state of the art” (code style guidelines, best practices, language standard) is constantly evolving and the project consciously chooses to evolve with them.
These guidelines attempt to provide a set of general rules that any contributor can choose to follow and implement before opening a pull request to reduce review effort and increase its chances for adoption.
## General Contribution Guidelines
The canonical language used by the project is “American English”, which mainly applies to source code and non-translated material. Examples include:
* Commit messages
* Names of constants, variables, and types
* Source code comments
* File names
Appropriate templates are presented to the user when opening a new issue report or pull request on GitHub which are required to be fully filled out to be taken into consideration by the project.
The template might require checking off items that might not be applicable e.g., the requirement to have the code properly formatted does not apply when the contribution has not changed any source code. **It is permissible and preferred to tick the box regardless**.
Authors of contributions to the OBS Project are expected to abide by the “OBS Project Code of Conduct” available at https://github.com/obsproject/obs-studio/blob/master/COC.rst.
## Pull Request Review Guidelines
In general reviews of pull requests are not limited to project members and can be undertaken by contributors and other GitHub users. Project members commonly coordinate reviews of pull requests at their own discretion in line with project needs and available bandwidth.
To reduce possible friction of the review process, a simple set of rules should be followed:
* **Do not** request reviews from specific community or project members yourself.
* It is permitted to ping other GitHub users to notify them of a PR and kindly ask them if they are willing (and able) to review it, all within reason.
* Reviewers might leave comments or ask for changes on specific lines of changed code. Once the comment has been addressed **do not unilaterally resolve** the comment.
* It is entirely the reviewer's decision whether they would consider the changes to be sufficient to address their concerns. Just having addressed a comment, does not resolve it.
* As a reviewer, make sure to regularly review the comments received on your reviews, check the code changes, and resolve your comments once they have been addressed. **Don't leave pull request authors hanging.**
* Project members will usually ping reviewers about unresolved review comments and resolve such comments themselves to unblock PRs from being merged.
* When addressing review comments through code changes, try to avoid force-pushes and use temporary commits which can be squashed later (see below).
* Unless necessary, avoid rebasing a pull request branch unless it is absolutely necessary. Rebasing will pull in changes from the base branch into the "changes since last push" of the pull request, making it harder for reviewers to check what changes have been pushed.
## Commit Authoring Guidelines
The project uses the “50/72” standard for commit messages, which requires that a commit title uses no more than 50 characters, while any consecutive line used for the commit description is allowed to use a maximum of 72 characters (with long lines manually broken up).
The “anatomy” of a correct commit message with description looks like this:
```
prefix: This is a commit title using 50 characters
This is a commit description line that is allowed to use up to 72 chars
```
Prefixes are commonly defined by the “module” the commit applies changes to, which has repercussions as to how commit contents are authored (see below).
* Changes made to code for “libobs” should use the “libobs” prefix, similarly changes made to “obs-ffmpeg” use “obs-ffmpeg”.
* Changes made to multiple modules in the “plugins” directory can use the “plugins” prefix, particularly if the changes are of a similar nature
* Changes made to files involved in continuous integration (CI) should use the “CI” prefix regardless of the directory name.
* Changes made to files used by the CMake build system should use the “cmake” prefix”
When in doubt about what the correct prefix for a change might be, the existing commit history might be of help, however project guidelines might have changed in the interim and thus a “past” commit message might not be acceptable for new contributions.
### Commit Messages
The commit message is the primary place for documentation of changes, not only as a summary of “what” was changed, but even more importantly “why” the changes were made and “why” specifically in the way they have been made.
As such the commit message should be written in such a way that it could easily pass as the “Description” and “Motivation and Context” required when opening a new pull request (and indeed platforms like GitHub will automatically copy the commit message into the opening post for a pull request with a single commit).
This ensures that documentation and context are encoded within the source tree and do not require external documents or platforms to ensure this information is not lost.
Many of these ideas match the ideas outlined in https://chris.beams.io/git-commit.
### Commit Content Authoring Guidelines
#### Code Style
All code contributions to the project need to follow code style guidelines appropriate to the languages used by the changed files. For C and C++ the project has chosen to adopt well-established code style guidelines as its baseline with specific exceptions and changes outlined in a [separate document](https://github.com/obsproject/obs-studio/blob/master/CODESTYLE.md).
The baseline rules for the common languages used by the project are:
* C code needs to follow the Linux Kernel Coding Style (https://github.com/torvalds/linux/blob/master/Documentation/process/coding-style.rst)
* C++ code needs to follow OBS Studios’ style guide. Familiarity with the Google C++ Style Guide (https://google.github.io/styleguide/cppguide.html) can serve as a baseline.
* Objective-C and Objective-C++ code needs to follow Apple’s ObjectiveC Conventions (https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Conventions/Conventions.html)
* Swift code needs to follow the API Design Guidelines (https://www.swift.org/documentation/api-design-guidelines/)
* **The column size limit for all code is 120 characters**
To help with the formatting of code, the project contains configuration files for code-formatters of several languages:
* `clang-format` can be used to correctly format C, C++, Objective-C, and Objective-C++ soure code
* `swift-format` can be used to correctly format Swift source code
* `gersemi` can be used to correctly format CMake source code
> [!NOTE]
> The “build-aux” directory in the project’s root directory contains scripts that can automate this process and are used by the project’s CI infrastructure to automatically check all contributions that changed corresponding source code files.
#### Commit Contents
The changes contained in a commit should attempt to represent logical “units of change” that make sense in isolation and do not necessarily require any preceding or following commits to work. Indeed if a single commit would potentially leave the project in a broken state (e.g., the project cannot be compiled, the application not run, or basic application functionality cannot function) then it is not a full “unit of change”.
* Changes to misspelled words or purely aesthetic changes should in principle not be individually capable of breaking the project in such a way and are permitted to be grouped into a single “large” commit
* If a larger change requires some foundation of fixes to the existing code to make sense, those fixes represent a “unit of change” that should be decoupled from the functional changes made on top of them. Even better, such a change should be encapsulated in its own, separate, pull request.
> [!IMPORTANT]
> During review and collaboration the need for additional changes can arise quickly. It is permitted (and preferred) to temporarily ignore these guidelines to quickly provide changes required to address review comments and add many small, iterative, commits to the pull request.
>
> Once all review comments have been addressed, the changes need to be squashed into existing commits or alternatively a new set of commits can be created from all accumulated changes to restore compliance with the guidelines again.
#### Co-Authorship
Sometimes an open pull request might have been "lost to time" even though its contents might still be a meaningful and positive change. When picking up such a change and authoring a new pull request to "finish it", the original author should be retained as much as possible:
* The original commit can be cherry-picked, which will retain the author information and only change the committer information
* If the original commit needs to be adjusted and changed as it will not apply to an updated code-base, its original author should be added as a "Co-Author" to the commit message.
```
libobs: Refactor audio implementation
Additional commit description
<..>
Co-authored-by: Name <git@address.TLD>
```
## AI/Machine Learning Policy
AI/machine learning systems such as Copilot, ChatGPT, and Claude, are prone to generating plausible-sounding, but wrong code that makes incorrect assumptions about OBS internals or APIs it interfaces with.
This means code generated by such systems will require human review and is likely to require human intervention. If the submitter is unable to undertake that work themselves due to a lack of understanding of the OBS codebase and/or programming, the submission has a high likelihood of being invalid. Such invalid submissions end up taking maintainers' time to review and respond away from legitimate submissions.
Additionally, such systems have been demonstrated to reproduce code contained in the training data, which may have been originally published under a license that would prohibit its inclusion in OBS.
Because of the above concerns, we have opted to take the following policy towards submissions with regard to the use of these AI tools:
* All text and content submitted to our project, which includes code, descriptions, issues, and comments, must be **human written**.
* The use of GitHub Copilot and other assistive AI technologies is heavily discouraged.
* Low-effort or incorrect submissions that are determined to have been generated by, or created with aid of such systems may lead to a ban from contributing to the repository or project as a whole.

339
obs-studio/COPYING 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.

1
obs-studio/INSTALL Normal file
View File

@ -0,0 +1 @@
For install instructions please visit https://github.com/obsproject/obs-studio/wiki/Install-Instructions

79
obs-studio/README.rst Normal file
View File

@ -0,0 +1,79 @@
OBS Studio <https://obsproject.com>
===================================
.. image:: https://github.com/obsproject/obs-studio/actions/workflows/push.yaml/badge.svg?branch=master
:alt: OBS Studio Build Status - GitHub Actions
:target: https://github.com/obsproject/obs-studio/actions/workflows/push.yaml?query=branch%3Amaster
.. image:: https://badges.crowdin.net/obs-studio/localized.svg
:alt: OBS Studio Translation Project Progress
:target: https://crowdin.com/project/obs-studio
.. image:: https://img.shields.io/discord/348973006581923840.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2
:alt: OBS Studio Discord Server
:target: https://obsproject.com/discord
What is OBS Studio?
-------------------
OBS Studio is software designed for capturing, compositing, encoding,
recording, and streaming video content, efficiently.
It's distributed under the GNU General Public License v2 (or any later
version) - see the accompanying COPYING file for more details.
Quick Links
-----------
- Website: https://obsproject.com
- Help/Documentation/Guides: https://github.com/obsproject/obs-studio/wiki
- Forums: https://obsproject.com/forum/
- Build Instructions: https://github.com/obsproject/obs-studio/wiki/Install-Instructions
- Developer/API Documentation: https://obsproject.com/docs
- Donating/backing/sponsoring: https://obsproject.com/contribute
- Bug Tracker: https://github.com/obsproject/obs-studio/issues
Contributing
------------
- If you would like to help fund or sponsor the project, you can do so
via `Patreon <https://www.patreon.com/obsproject>`_, `OpenCollective
<https://opencollective.com/obsproject>`_, or `PayPal
<https://www.paypal.me/obsproject>`_. See our `contribute page
<https://obsproject.com/contribute>`_ for more information.
- If you wish to contribute code to the project, please make sure to
read the coding and commit guidelines:
https://github.com/obsproject/obs-studio/blob/master/CONTRIBUTING.md
- Code for the project follows the code style guidelines, located
here: https://github.com/obsproject/obs-studio/blob/master/CODESTYLE.md
- Developer/API documentation can be found here:
https://obsproject.com/docs
- If you wish to contribute translations, do not submit pull requests.
Instead, please use Crowdin. For more information read this page:
https://obsproject.com/wiki/How-To-Contribute-Translations-For-OBS
- Contributors to OBS Studio and related repositories are expected to
follow our Code of Conduct, which can be read here:
https://github.com/obsproject/obs-studio/blob/master/COC.rst
- Other ways to contribute are by helping people out with support on
our forums or in our community chat. Please limit support to topics
you fully understand -- bad advice is worse than no advice. When it
comes to something that you don't fully know or understand, please
defer to the official help or official channels.
SAST Tools
----------
`PVS-Studio <https://pvs-studio.com/pvs-studio/?utm_source=website&utm_medium=github&utm_campaign=open_source>`_ - static analyzer for C, C++, C#, and Java code.

View File

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

View File

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

View File

@ -0,0 +1,16 @@
autoload -Uz log_info
if (( ! ${+_log_group} )) typeset -g _log_group=0
if (( ${+CI} )) {
if (( _log_group )) {
print "::endgroup::"
typeset -g _log_group=0
}
if (( # )) {
print "::group::${@}"
typeset -g _log_group=1
}
} else {
if (( # )) log_info ${@}
}

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"
}

View File

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

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 ${@}"
}

Some files were not shown because too many files have changed in this diff Show More