mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-08-29 04:36:21 -05:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b74e9d2e2a | ||
|
|
7cb95cb3f6 | ||
|
|
160d19d6c1 | ||
|
|
a0550ca141 | ||
|
|
ac68fc016d | ||
|
|
5170307f72 | ||
|
|
3af0e51bd2 | ||
|
|
f34d1fe081 | ||
|
|
83c72ebba5 | ||
|
|
6ca8e6b3ea | ||
|
|
ba24fc2fab | ||
|
|
7f28b22599 | ||
|
|
4345db0d2e | ||
|
|
eddc10c3e2 | ||
|
|
46eaada0c4 | ||
|
|
f8b2dea270 | ||
|
|
90ebf0e0f8 | ||
|
|
1a819c807e | ||
|
|
3220809c04 | ||
|
|
72d5255c9e | ||
|
|
5995563a80 | ||
|
|
347743d93a | ||
|
|
3c067ce723 | ||
|
|
8dd2170dfa | ||
|
|
5f3a3dd5cf | ||
|
|
8ad3563963 | ||
|
|
492128ef86 | ||
|
|
b4936274f2 | ||
|
|
64ccd5ba53 | ||
|
|
7ae88f7560 | ||
|
|
ae70af26d1 | ||
|
|
483bdc237c | ||
|
|
2c564b47f4 | ||
|
|
fc80d0b5f3 | ||
|
|
d094c189d3 | ||
|
|
40d8819e77 | ||
|
|
4a3d019f06 | ||
|
|
5366a4a6ed | ||
|
|
e8230d8e5b | ||
|
|
ebdbb9011f | ||
|
|
e43793354e |
81
.github/scripts/.build.zsh
vendored
81
.github/scripts/.build.zsh
vendored
@@ -173,23 +173,29 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
|
||||
read -r product_name product_version <<< \
|
||||
"$(jq -r '. | {name, version} | join(" ")' ${buildspec_file})"
|
||||
|
||||
local opencv_dir="${project_root}/deps/opencv"
|
||||
local opencv_build_dir="${opencv_dir}/build_${target##*-}"
|
||||
|
||||
|
||||
|
||||
case ${host_os} {
|
||||
macos)
|
||||
sed -i '' \
|
||||
"s/project(\(.*\) VERSION \(.*\))/project(${product_name} VERSION ${product_version})/" \
|
||||
"${project_root}/CMakeLists.txt"
|
||||
|
||||
# Note: IPP must be set to OFF to avoid build failures on ARM architectures
|
||||
local opencv_dir="${project_root}/deps/opencv"
|
||||
local opencv_build_dir="${opencv_dir}/build_${target##*-}"
|
||||
|
||||
local -a opencv_cmake_args=(
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DBUILD_LIST=core,imgproc,objdetect
|
||||
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
|
||||
-DWITH_IPP=OFF
|
||||
)
|
||||
|
||||
if [ "${target}" != "macos-x86_64" ]; then
|
||||
opencv_cmake_args+=(-DWITH_IPP=OFF)
|
||||
fi
|
||||
|
||||
pushd ${opencv_dir}
|
||||
log_info "Configure OpenCV ..."
|
||||
cmake -S . -B ${opencv_build_dir} -G ${generator} ${opencv_cmake_args}
|
||||
@@ -198,9 +204,73 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
|
||||
cmake --build ${opencv_build_dir} --config Release
|
||||
|
||||
log_info "Installing OpenCV..."
|
||||
cmake --install ${opencv_build_dir} --config Release --prefix ${opencv_build_dir}
|
||||
cmake --install ${opencv_build_dir} --config Release || true
|
||||
popd
|
||||
|
||||
local leptonica_dir="${project_root}/deps/leptonica"
|
||||
local leptonica_build_dir="${leptonica_dir}/build_${target##*-}"
|
||||
|
||||
local -a leptonica_cmake_args=(
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
|
||||
-DSW_BUILD=OFF
|
||||
-DOPENJPEG_SUPPORT=OFF
|
||||
-DLIBWEBP_SUPPORT=OFF
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_GIF=TRUE
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_JPEG=TRUE
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_PNG=TRUE
|
||||
)
|
||||
|
||||
pushd ${leptonica_dir}
|
||||
log_info "Configure Leptonica ..."
|
||||
cmake -S . -B ${leptonica_build_dir} -G ${generator} ${leptonica_cmake_args}
|
||||
|
||||
log_info "Building Leptonica ..."
|
||||
cmake --build ${leptonica_build_dir} --config Release
|
||||
|
||||
log_info "Installing Leptonica..."
|
||||
# Workaround for "unknown file attribute: H" errors when running install
|
||||
cmake --install ${leptonica_build_dir} --config Release || :
|
||||
popd
|
||||
|
||||
local tesseract_dir="${project_root}/deps/tesseract"
|
||||
local tesseract_build_dir="${tesseract_dir}/build_${target##*-}"
|
||||
|
||||
local -a tesseract_cmake_args=(
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
|
||||
-DSW_BUILD=OFF
|
||||
-DBUILD_TRAINING_TOOLS=OFF
|
||||
)
|
||||
|
||||
if [ "${target}" != "macos-x86_64" ]; then
|
||||
tesseract_cmake_args+=(
|
||||
-DCMAKE_SYSTEM_PROCESSOR=aarch64
|
||||
-DHAVE_AVX=FALSE
|
||||
-DHAVE_AVX2=FALSE
|
||||
-DHAVE_AVX512F=FALSE
|
||||
-DHAVE_FMA=FALSE
|
||||
-DHAVE_SSE4_1=FALSE
|
||||
-DHAVE_NEON=TRUE
|
||||
)
|
||||
sed -i'.original' 's/HAVE_NEON FALSE/HAVE_NEON TRUE/g' "${tesseract_dir}/CMakeLists.txt"
|
||||
fi
|
||||
|
||||
pushd ${tesseract_dir}
|
||||
log_info "Configure Tesseract ..."
|
||||
cmake -S . -B ${tesseract_build_dir} -G ${generator} ${tesseract_cmake_args}
|
||||
|
||||
log_info "Building Tesseract ..."
|
||||
#cmake --build ${tesseract_build_dir} --config Release --target libtesseract
|
||||
cmake --build ${tesseract_build_dir} --config Release
|
||||
|
||||
log_info "Installing Tesseract..."
|
||||
#cmake --install ${tesseract_build_dir} --config Release --component libtesseract
|
||||
cmake --install ${tesseract_build_dir} --config Release
|
||||
popd
|
||||
;;
|
||||
linux)
|
||||
sed -i'' \
|
||||
@@ -242,7 +312,6 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
|
||||
-DOBS_CODESIGN_LINKER=ON
|
||||
-DOBS_BUNDLE_CODESIGN_IDENTITY="${CODESIGN_IDENT:--}"
|
||||
-DOpenCV_DIR="${opencv_build_dir}"
|
||||
)
|
||||
|
||||
;;
|
||||
|
||||
101
.github/scripts/Build-Windows.ps1
vendored
101
.github/scripts/Build-Windows.ps1
vendored
@@ -37,7 +37,7 @@ function Build {
|
||||
|
||||
$UtilityFunctions = Get-ChildItem -Path $PSScriptRoot/utils.pwsh/*.ps1 -Recurse
|
||||
|
||||
foreach($Utility in $UtilityFunctions) {
|
||||
foreach ($Utility in $UtilityFunctions) {
|
||||
Write-Debug "Loading $($Utility.FullName)"
|
||||
. $Utility.FullName
|
||||
}
|
||||
@@ -57,18 +57,22 @@ function Build {
|
||||
$CmakeGenerator = $script:VisualStudioVersion
|
||||
}
|
||||
|
||||
$DepsPath = "plugin-deps-${script:DepsVersion}-qt${script:QtVersion}-${script:Target}"
|
||||
$DepInstallPath = "$(Resolve-Path -Path ${ProjectRoot}/../obs-build-dependencies/${DepsPath})"
|
||||
|
||||
$OpenCVPath = "${ProjectRoot}/deps/opencv"
|
||||
$OpenCVBuildPath = "${OpenCVPath}/build"
|
||||
|
||||
Push-Location -Stack BuildOpenCVTemp
|
||||
if ( ! ( ( $SkipAll ) -or ( $SkipBuild ) ) ) {
|
||||
Ensure-Location $ProjectRoot
|
||||
|
||||
$OpenCVPath = "${ProjectRoot}/deps/opencv"
|
||||
$OpenCVBuildPath = "${OpenCVPath}/build"
|
||||
$OpenCVCmakeArgs = @(
|
||||
'-G', $CmakeGenerator
|
||||
"-DCMAKE_SYSTEM_VERSION=${script:PlatformSDK}"
|
||||
"-DCMAKE_GENERATOR_PLATFORM=$(if (${script:Target} -eq "x86") { "Win32" } else { "x64" })"
|
||||
"-DCMAKE_BUILD_TYPE=Release"
|
||||
"-DCMAKE_PREFIX_PATH:PATH=$(Resolve-Path -Path ${ProjectRoot}/../obs-build-dependencies/${DepsPath})"
|
||||
"-DCMAKE_PREFIX_PATH:PATH=${DepInstallPath}"
|
||||
"-DBUILD_LIST=core,imgproc,objdetect"
|
||||
)
|
||||
|
||||
@@ -80,18 +84,92 @@ function Build {
|
||||
)
|
||||
|
||||
if ( $VerbosePreference -eq 'Continue' ) {
|
||||
$OpenCVCmakeArgs+=('--verbose')
|
||||
$OpenCVCmakeArgs += ('--verbose')
|
||||
}
|
||||
|
||||
Log-Information "Building OpenCV..."
|
||||
Invoke-External cmake --build "${OpenCVBuildPath}" @OpenCVCmakeArgs
|
||||
}
|
||||
Log-Information "Install OpenCV}..."
|
||||
Invoke-External cmake --install "${OpenCVBuildPath}" --prefix "${OpenCVBuildPath}" @OpenCVCmakeArgs
|
||||
Invoke-External cmake --install "${OpenCVBuildPath}" --prefix "${DepInstallPath}" @OpenCVCmakeArgs
|
||||
|
||||
$LeptonicaPath = "${ProjectRoot}/deps/leptonica"
|
||||
$LeptonicaBuildPath = "${LeptonicaPath}/build"
|
||||
|
||||
Push-Location -Stack BuildLeptonicaTemp
|
||||
if ( ! ( ( $SkipAll ) -or ( $SkipBuild ) ) ) {
|
||||
Ensure-Location $ProjectRoot
|
||||
|
||||
$LeptonicaCmakeArgs = @(
|
||||
'-G', $CmakeGenerator
|
||||
"-DCMAKE_SYSTEM_VERSION=${script:PlatformSDK}"
|
||||
"-DCMAKE_GENERATOR_PLATFORM=$(if (${script:Target} -eq "x86") { "Win32" } else { "x64" })"
|
||||
"-DCMAKE_BUILD_TYPE=Release"
|
||||
"-DCMAKE_PREFIX_PATH:PATH=${DepInstallPath}"
|
||||
"-DCMAKE_INSTALL_PREFIX:PATH=${DepInstallPath}"
|
||||
"-DSW_BUILD=OFF"
|
||||
"-DLIBWEBP_SUPPORT=OFF"
|
||||
)
|
||||
|
||||
Log-Information "Configuring leptonica..."
|
||||
Invoke-External cmake -S ${LeptonicaPath} -B ${LeptonicaBuildPath} @LeptonicaCmakeArgs
|
||||
|
||||
$LeptonicaCmakeArgs = @(
|
||||
'--config', "Release"
|
||||
)
|
||||
|
||||
if ( $VerbosePreference -eq 'Continue' ) {
|
||||
$LeptonicaCmakeArgs += ('--verbose')
|
||||
}
|
||||
|
||||
Log-Information "Building leptonica..."
|
||||
Invoke-External cmake --build "${LeptonicaBuildPath}" @LeptonicaCmakeArgs
|
||||
}
|
||||
Log-Information "Install leptonica..."
|
||||
Invoke-External cmake --install "${LeptonicaBuildPath}" --prefix "${DepInstallPath}" @LeptonicaCmakeArgs
|
||||
|
||||
Push-Location -Stack BuildTesseractTemp
|
||||
if ( ! ( ( $SkipAll ) -or ( $SkipBuild ) ) ) {
|
||||
Ensure-Location $ProjectRoot
|
||||
|
||||
$TesseractPath = "${ProjectRoot}/deps/tesseract"
|
||||
$TesseractBuildPath = "${TesseractPath}/build"
|
||||
|
||||
# Explicitly disable PkgConfig and tiff as it will lead build errors
|
||||
$TesseractCmakeArgs = @(
|
||||
'-G', $CmakeGenerator
|
||||
"-DCMAKE_SYSTEM_VERSION=${script:PlatformSDK}"
|
||||
"-DCMAKE_GENERATOR_PLATFORM=$(if (${script:Target} -eq "x86") { "Win32" } else { "x64" })"
|
||||
"-DCMAKE_BUILD_TYPE=Release"
|
||||
"-DCMAKE_PREFIX_PATH:PATH=${DepInstallPath}"
|
||||
"-DCMAKE_INSTALL_PREFIX:PATH=${DepInstallPath}"
|
||||
"-DSW_BUILD=OFF"
|
||||
"-DDISABLE_CURL=ON"
|
||||
"-DBUILD_TRAINING_TOOLS=OFF"
|
||||
"-DCMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE"
|
||||
"-DCMAKE_DISABLE_FIND_PACKAGE_PkgConfig=TRUE"
|
||||
)
|
||||
|
||||
Log-Information "Configuring tesseract..."
|
||||
Invoke-External cmake -S ${TesseractPath} -B ${TesseractBuildPath} @TesseractCmakeArgs
|
||||
|
||||
$TesseractCmakeArgs = @(
|
||||
'--config', "Release"
|
||||
)
|
||||
|
||||
if ( $VerbosePreference -eq 'Continue' ) {
|
||||
$TesseractCmakeArgs += ('--verbose')
|
||||
}
|
||||
|
||||
Log-Information "Building tesseract..."
|
||||
Invoke-External cmake --build "${TesseractBuildPath}" @TesseractCmakeArgs
|
||||
}
|
||||
Log-Information "Install tesseract..."
|
||||
Invoke-External cmake --install "${TesseractBuildPath}" --prefix "${DepInstallPath}" @TesseractCmakeArgs
|
||||
|
||||
(Get-Content -Path ${ProjectRoot}/CMakeLists.txt -Raw) `
|
||||
-replace "project\((.*) VERSION (.*)\)", "project(${ProductName} VERSION ${ProductVersion})" `
|
||||
| Out-File -Path ${ProjectRoot}/CMakeLists.txt
|
||||
| Out-File -Path ${ProjectRoot}/CMakeLists.txt
|
||||
|
||||
Setup-Obs
|
||||
|
||||
@@ -99,18 +177,13 @@ function Build {
|
||||
if ( ! ( ( $SkipAll ) -or ( $SkipBuild ) ) ) {
|
||||
Ensure-Location $ProjectRoot
|
||||
|
||||
$OpenCVPath = "${ProjectRoot}/deps/opencv"
|
||||
$OpenCVBuildPath = "${OpenCVPath}/build"
|
||||
|
||||
$DepsPath = "plugin-deps-${script:DepsVersion}-qt${script:QtVersion}-${script:Target}"
|
||||
$CmakeArgs = @(
|
||||
'-G', $CmakeGenerator
|
||||
"-DCMAKE_SYSTEM_VERSION=${script:PlatformSDK}"
|
||||
"-DCMAKE_GENERATOR_PLATFORM=$(if (${script:Target} -eq "x86") { "Win32" } else { "x64" })"
|
||||
"-DCMAKE_BUILD_TYPE=${Configuration}"
|
||||
"-DCMAKE_PREFIX_PATH:PATH=$(Resolve-Path -Path "${ProjectRoot}/../obs-build-dependencies/${DepsPath}")"
|
||||
"-DCMAKE_PREFIX_PATH:PATH=${DepInstallPath}"
|
||||
"-DQT_VERSION=${script:QtVersion}"
|
||||
"-DOpenCV_DIR=${OpenCVBuildPath}"
|
||||
)
|
||||
|
||||
Log-Debug "Attempting to configure OBS with CMake arguments: $($CmakeArgs | Out-String)"
|
||||
@@ -122,7 +195,7 @@ function Build {
|
||||
)
|
||||
|
||||
if ( $VerbosePreference -eq 'Continue' ) {
|
||||
$CmakeArgs+=('--verbose')
|
||||
$CmakeArgs += ('--verbose')
|
||||
}
|
||||
|
||||
Log-Information "Building ${ProductName}..."
|
||||
|
||||
1
.github/scripts/utils.zsh/setup_linux
vendored
1
.github/scripts/utils.zsh/setup_linux
vendored
@@ -38,6 +38,7 @@ if (( ! (${skips[(Ie)all]} + ${skips[(Ie)deps]}) )) {
|
||||
libxtst-dev \
|
||||
libxss-dev \
|
||||
libopencv-dev \
|
||||
libtesseract-dev \
|
||||
libprocps-dev
|
||||
|
||||
local -a _qt_packages=()
|
||||
|
||||
24
.github/workflows/main.yml
vendored
24
.github/workflows/main.yml
vendored
@@ -113,7 +113,11 @@ jobs:
|
||||
id: ccache-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.ccache
|
||||
path: |
|
||||
${{ github.workspace }}/.ccache
|
||||
${{ github.workspace }}/plugin/deps/opencv/build_*
|
||||
${{ github.workspace }}/plugin/deps/leptonica/build_*
|
||||
${{ github.workspace }}/plugin/deps/tesseract/build*
|
||||
key: macos-${{ matrix.arch }}-ccache-plugin-${{ steps.setup.outputs.ccacheDate }}
|
||||
restore-keys: |
|
||||
macos-${{ matrix.arch }}-ccache-plugin-
|
||||
@@ -221,7 +225,8 @@ jobs:
|
||||
id: ccache-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.ccache
|
||||
path: |
|
||||
${{ github.workspace }}/.ccache
|
||||
key: linux-${{ matrix.arch }}-ccache-plugin-${{ steps.setup.outputs.ccacheDate }}
|
||||
restore-keys: |
|
||||
linux-${{ matrix.arch }}-ccache-plugin-
|
||||
@@ -301,9 +306,24 @@ jobs:
|
||||
id: setup
|
||||
run: |
|
||||
## SETUP ENVIRONMENT SCRIPT
|
||||
$CcacheDate = (Get-Date -Format "yyyy-M-d")
|
||||
"ccacheDate=${CcacheDate}" >> $env:GITHUB_OUTPUT
|
||||
$CommitHash = (git rev-parse HEAD)[0..8] -join ''
|
||||
"commitHash=${CommitHash}" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Restore Compilation Cache
|
||||
id: ccache-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/.ccache
|
||||
${{ github.workspace }}/plugin/deps/opencv/build_*
|
||||
${{ github.workspace }}/plugin/deps/leptonica/build_*
|
||||
${{ github.workspace }}/plugin/deps/tesseract/build*
|
||||
key: windows-${{ matrix.arch }}-ccache-plugin-${{ steps.setup.outputs.ccacheDate }}
|
||||
restore-keys: |
|
||||
windows-${{ matrix.arch }}-ccache-plugin-
|
||||
|
||||
- name: Check for GitHub Labels
|
||||
id: seekingTesters
|
||||
working-directory: ${{ github.workspace }}/plugin
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -10,3 +10,9 @@
|
||||
[submodule "deps/openvr"]
|
||||
path = deps/openvr
|
||||
url = https://github.com/ValveSoftware/openvr.git
|
||||
[submodule "deps/leptonica"]
|
||||
path = deps/leptonica
|
||||
url = https://github.com/DanBloomberg/leptonica.git
|
||||
[submodule "deps/tesseract"]
|
||||
path = deps/tesseract
|
||||
url = https://github.com/tesseract-ocr/tesseract.git
|
||||
|
||||
@@ -113,6 +113,8 @@ target_sources(
|
||||
src/macro-core/macro-action-plugin-state.hpp
|
||||
src/macro-core/macro-action-profile.cpp
|
||||
src/macro-core/macro-action-profile.hpp
|
||||
src/macro-core/macro-action-projector.cpp
|
||||
src/macro-core/macro-action-projector.hpp
|
||||
src/macro-core/macro-action-random.cpp
|
||||
src/macro-core/macro-action-random.hpp
|
||||
src/macro-core/macro-action-recording.cpp
|
||||
|
||||
@@ -22,5 +22,9 @@
|
||||
</array>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13</string>
|
||||
<key>com.apple.security.automation.apple-events</key>
|
||||
<true/>
|
||||
<key>NSAppleEventsUsageDescription</key>
|
||||
<string>Advanced scene switcher needs additional permissions to be able to run AppleScript applications</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -28,7 +28,10 @@ AdvSceneSwitcher.generalTab.generalBehavior.cooldown="Nach Ausführen von Aktion
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.cooldownHint="In diesem Zeitraum werden potentielle erfüllte Bedingungen ignoriert!"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.verboseLogging="Ausführliches Logging"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.saveWindowGeo="Fensterposition und -größe speichern"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.showTrayNotifications="Benachrichtigungen im System-Tray anzeigen"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.disableUIHints="Deaktiviere UI Tipps"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.hideLegacyTabs="Tabs verstecken, die in Makros repräsentiert sind"
|
||||
AdvSceneSwitcher.generalTab.matchBehavior="Verhalten anpassen"
|
||||
AdvSceneSwitcher.generalTab.priority="Priorität"
|
||||
AdvSceneSwitcher.generalTab.priority.description="Szenenwechselmethoden sortiert nach Priorität"
|
||||
AdvSceneSwitcher.generalTab.priority.threadPriority="Benutze Thread Priorität"
|
||||
@@ -51,11 +54,542 @@ AdvSceneSwitcher.generalTab.priority.media="Medien"
|
||||
AdvSceneSwitcher.generalTab.priority.time="Zeit"
|
||||
AdvSceneSwitcher.generalTab.priority.audio="Audio"
|
||||
AdvSceneSwitcher.generalTab.priority.video="Video"
|
||||
AdvSceneSwitcher.generalTab.priority.macro="Makro"
|
||||
AdvSceneSwitcher.generalTab.transition="Übergänge"
|
||||
AdvSceneSwitcher.generalTab.setTransitionBy="Beim Ändern des Szenenübergangs:"
|
||||
AdvSceneSwitcher.generalTab.transitionOverride="Verwende Übergangsüberschreibungen"
|
||||
AdvSceneSwitcher.generalTab.adjustActiveTransitionType="Wechsle den aktiven Szenenübergangtyp"
|
||||
AdvSceneSwitcher.generalTab.transitionBehaviorSelectionError="Mindestens eine Option muss aktiv sein:\n\n - Verwende Übergangsüberschreibungen\n\n - Wechsle den aktiven Szenenübergangtyp"
|
||||
|
||||
; Macro Tab
|
||||
AdvSceneSwitcher.macroTab.title="Makro"
|
||||
AdvSceneSwitcher.macroTab.macros="Makros"
|
||||
AdvSceneSwitcher.macroTab.priorityWarning="Hinweis: Es wird empfohlen, Makros als Funktion mit höchster Priorität zu konfigurieren.\nDiese Einstellung kann im Allgemein-Tab geändert werden."
|
||||
AdvSceneSwitcher.macroTab.help="Mit Makros können eine Reihe von Aktionen in Abhängigkeit von mehreren Bedingungen ausgeführt werden.\n\nKlicke auf das hervorgehobene Plus-Symbol, um ein neues Makro hinzuzufügen."
|
||||
AdvSceneSwitcher.macroTab.editConditionHelp="In diesem Abschnitt können Bedingungen für die Makros definiert werden.\n\nWähle auf der linken Seite ein vorhandenes Makro aus oder füge ein neues hinzu.\nKlicke dann auf die Plus-Schaltfläche unten, um eine neue Bedingung hinzuzufügen."
|
||||
AdvSceneSwitcher.macroTab.editActionHelp="In diesem Abschnitt können Aktionen für die Makros definiert werden.\n\nWähle auf der linken Seite ein vorhandenes Makro aus oder füge ein neues hinzu.\nKlicke dann auf die Plus-Schaltfläche unten, um eine neue Aktion hinzuzufügen."
|
||||
AdvSceneSwitcher.macroTab.edit="Makro bearbeiten"
|
||||
AdvSceneSwitcher.macroTab.edit.logic="Logik-Typ:"
|
||||
AdvSceneSwitcher.macroTab.edit.condition="Bedingungs-Typ:"
|
||||
AdvSceneSwitcher.macroTab.edit.action="Aktions-Typ:"
|
||||
AdvSceneSwitcher.macroTab.add="Neues Makro hinzufügen"
|
||||
AdvSceneSwitcher.macroTab.name="Name:"
|
||||
AdvSceneSwitcher.macroTab.run="Makro ausführen"
|
||||
AdvSceneSwitcher.macroTab.runFail="Ausführen von \"%1\" fehlgeschlagen!\nEntweder ist eine der Aktionen fehlgeschlagen oder das Makro wird bereits ausgeführt."
|
||||
AdvSceneSwitcher.macroTab.runInParallel="Parallel zu anderen Makros ausführen"
|
||||
AdvSceneSwitcher.macroTab.onChange="Nur bei Änderung ausführen"
|
||||
AdvSceneSwitcher.macroTab.defaultname="Makro %1"
|
||||
AdvSceneSwitcher.macroTab.defaultGroupName="Gruppe %1"
|
||||
AdvSceneSwitcher.macroTab.exists="Makro-Name existiert bereits"
|
||||
AdvSceneSwitcher.macroTab.groupDeleteConfirm="Sicher, dass \"%1\" und alle zugehörigen Elemente gelöscht werden?"
|
||||
AdvSceneSwitcher.macroTab.copy="Kopie des aktuellen Makros erstellen"
|
||||
AdvSceneSwitcher.macroTab.group="Ausgewählte Elemente gruppieren"
|
||||
AdvSceneSwitcher.macroTab.ungroup="Gruppierung ausgewählter Gruppen aufheben"
|
||||
AdvSceneSwitcher.macroTab.expandAll="Alle erweitern"
|
||||
AdvSceneSwitcher.macroTab.collapseAll="Alle einklappen"
|
||||
AdvSceneSwitcher.macroTab.maximize="Maximieren"
|
||||
AdvSceneSwitcher.macroTab.minimize="Minimieren"
|
||||
AdvSceneSwitcher.macroTab.highlightSettings="Visuelle Einstellungen"
|
||||
AdvSceneSwitcher.macroTab.hotkeySettings="Hotkey-Einstellungen"
|
||||
AdvSceneSwitcher.macroTab.highlightExecutedMacros="Kürzlich ausgeführte Makros hervorheben"
|
||||
AdvSceneSwitcher.macroTab.highlightTrueConditions="Bedingungen des aktuell ausgewählten Makros hervorheben, die kürzlich als wahr bewertet wurden"
|
||||
AdvSceneSwitcher.macroTab.highlightPerformedActions="Zuletzt ausgeführte Aktionen des aktuell ausgewählten Makros hervorheben"
|
||||
AdvSceneSwitcher.macroTab.newMacroRegisterHotkey="Hotkeys zur Steuerung des Pausen-Status neuer Makros registrieren"
|
||||
AdvSceneSwitcher.macroTab.currentDisableHotkeys="Hotkeys zur Steuerung des Pausen-Status ausgewählter Makros registrieren"
|
||||
|
||||
; Macro List
|
||||
AdvSceneSwitcher.macroList.deleted="gelöscht"
|
||||
AdvSceneSwitcher.macroList.duplicate="\"%1\" ist bereits ausgewählt!"
|
||||
|
||||
; Macro Logic
|
||||
AdvSceneSwitcher.logic.none="Eintrag ignorieren"
|
||||
AdvSceneSwitcher.logic.and="Und"
|
||||
AdvSceneSwitcher.logic.or="Oder"
|
||||
AdvSceneSwitcher.logic.andNot="Und nicht"
|
||||
AdvSceneSwitcher.logic.orNot="Oder nicht"
|
||||
AdvSceneSwitcher.logic.rootNone="Wenn"
|
||||
AdvSceneSwitcher.logic.not="Wenn nicht"
|
||||
|
||||
; Macro Conditions
|
||||
AdvSceneSwitcher.condition.audio="Audio"
|
||||
AdvSceneSwitcher.condition.audio.state.below="unter"
|
||||
AdvSceneSwitcher.condition.audio.state.exact="genau"
|
||||
AdvSceneSwitcher.condition.audio.state.above="über"
|
||||
AdvSceneSwitcher.condition.audio.state.mute="stummgeschaltet"
|
||||
AdvSceneSwitcher.condition.audio.state.unmute="nicht stummgeschaltet"
|
||||
AdvSceneSwitcher.condition.audio.type.output="Ausgabe-Lautstärke"
|
||||
AdvSceneSwitcher.condition.audio.type.volume="Konfigurierter Lautstärkepegel"
|
||||
AdvSceneSwitcher.condition.audio.type.syncOffset="Sync-Offset"
|
||||
AdvSceneSwitcher.condition.audio.type.monitor="Audio-Monitoring"
|
||||
AdvSceneSwitcher.condition.audio.type.balance="Audio-Balance"
|
||||
AdvSceneSwitcher.condition.audio.entry="{{checkType}} von {{audioSources}} ist {{condition}}{{volume}}{{syncOffset}}{{monitorTypes}}"
|
||||
AdvSceneSwitcher.condition.cursor="Cursor"
|
||||
AdvSceneSwitcher.condition.cursor.type.region="ist in der Region"
|
||||
AdvSceneSwitcher.condition.cursor.type.moving="bewegt sich"
|
||||
AdvSceneSwitcher.condition.cursor.type.click="geklickt"
|
||||
AdvSceneSwitcher.condition.cursor.button.left="Linke Maustaste"
|
||||
AdvSceneSwitcher.condition.cursor.button.middle="Mittlere Maustaste"
|
||||
AdvSceneSwitcher.condition.cursor.button.right="Rechte Maustaste"
|
||||
AdvSceneSwitcher.condition.cursor.showFrame="Rahmen anzeigen"
|
||||
AdvSceneSwitcher.condition.cursor.hideFrame="Rahmen ausblenden"
|
||||
AdvSceneSwitcher.condition.cursor.entry.line1="Cursor {{conditions}}{{buttons}}{{minX}}{{minY}}{{maxX}}{{maxY}}{{toggleFrameButton}}"
|
||||
AdvSceneSwitcher.condition.cursor.entry.line2="Cursor ist aktuell bei {{xPos}} x {{yPos}}"
|
||||
AdvSceneSwitcher.condition.scene="Szene"
|
||||
AdvSceneSwitcher.condition.scene.type.current="Aktuelle Szene ist"
|
||||
AdvSceneSwitcher.condition.scene.type.previous="Vorherige Szene ist"
|
||||
AdvSceneSwitcher.condition.scene.type.changed="Szene geändert"
|
||||
AdvSceneSwitcher.condition.scene.type.notChanged="Szene hat sich nicht verändert"
|
||||
AdvSceneSwitcher.condition.scene.type.currentPattern="Aktuelle Szene entspricht"
|
||||
AdvSceneSwitcher.condition.scene.type.previousPattern="Vorherige Szene entspricht"
|
||||
AdvSceneSwitcher.condition.scene.currentSceneTransitionBehaviour="Während des Übergangs die Ziel-Szene überprüfen"
|
||||
AdvSceneSwitcher.condition.scene.previousSceneTransitionBehaviour="Während des Übergangs die Quell-Szene überprüfen"
|
||||
AdvSceneSwitcher.condition.scene.entry.line1="{{sceneType}}{{scenes}}{{pattern}}"
|
||||
AdvSceneSwitcher.condition.scene.entry.line2="{{useTransitionTargetScene}}"
|
||||
AdvSceneSwitcher.condition.window="Fenster"
|
||||
AdvSceneSwitcher.condition.window.entry.line1="{{windows}} existiert und ..."
|
||||
AdvSceneSwitcher.condition.window.entry.line2="... ist {{fullscreen}} Vollbild {{maximized}} Maximiert {{focused}} Fokusiert {{windowFocusChanged}} Vordergrundfenster geändert"
|
||||
AdvSceneSwitcher.condition.window.entry.line3="Aktuelles Vordergrundfenster: {{focusWindow}}"
|
||||
AdvSceneSwitcher.condition.file="Datei"
|
||||
AdvSceneSwitcher.condition.file.type.match="entspricht"
|
||||
AdvSceneSwitcher.condition.file.type.contentChange="Inhalt geändert"
|
||||
AdvSceneSwitcher.condition.file.type.dateChange="Änderungsdatum geändert"
|
||||
AdvSceneSwitcher.condition.file.remote="Entfernte Datei"
|
||||
AdvSceneSwitcher.condition.file.local="Lokale Datei"
|
||||
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}"
|
||||
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
|
||||
AdvSceneSwitcher.condition.file.entry.line3="{{useRegex}} {{checkModificationDate}} {{checkFileContent}}"
|
||||
AdvSceneSwitcher.condition.media="Medien"
|
||||
AdvSceneSwitcher.condition.media.source="Quelle"
|
||||
AdvSceneSwitcher.condition.media.anyOnScene="Beliebige Medienquelle in"
|
||||
AdvSceneSwitcher.condition.media.allOnScene="Alle Medienquellen in"
|
||||
AdvSceneSwitcher.condition.media.matchOnChange="Nur bei Änderung übereinstimmen (Hinweis: Diese Option wird in einer zukünftigen Version entfernt werden - bitte stattdessen Dauer-Modifikatoren verwenden)"
|
||||
AdvSceneSwitcher.condition.media.inconsistencyInfo="Leider verhalten sich nicht alle Medien-Quell-Typen gleich (z.B. Medien-Quelle vs. VLC-Video-Quelle \"Gestoppt\"-Status).\nBitte experimentieren, was in eurem Setup funktioniert!"
|
||||
AdvSceneSwitcher.condition.media.entry="{{sourceTypes}}{{mediaSources}}{{scenes}}Status ist{{states}}und{{timeRestrictions}}{{time}}"
|
||||
AdvSceneSwitcher.condition.video="Video"
|
||||
AdvSceneSwitcher.condition.video.condition.match="entspricht genau"
|
||||
AdvSceneSwitcher.condition.video.condition.differ="stimmt nicht überein"
|
||||
AdvSceneSwitcher.condition.video.condition.hasChanged="hat sich geändert"
|
||||
AdvSceneSwitcher.condition.video.condition.hasNotChanged="hat sich nicht verändert"
|
||||
AdvSceneSwitcher.condition.video.condition.noImage="hat keine Ausgabe"
|
||||
AdvSceneSwitcher.condition.video.condition.pattern="entspricht dem Muster"
|
||||
AdvSceneSwitcher.condition.video.condition.object="enthält Objekt"
|
||||
AdvSceneSwitcher.condition.video.condition.brightness="Helligkeit"
|
||||
AdvSceneSwitcher.condition.video.askFileAction="Vorhandene Datei verwenden oder einen Screenshot der aktuell ausgewählten Quelle erstellen?"
|
||||
AdvSceneSwitcher.condition.video.askFileAction.file="Vorhandene Datei verwenden"
|
||||
AdvSceneSwitcher.condition.video.askFileAction.screenshot="Screenshot erstellen"
|
||||
AdvSceneSwitcher.condition.video.reduceLatency="Reduzierung der Abgleichslatenz"
|
||||
AdvSceneSwitcher.condition.video.reduceLatency.tooltip="Durch die Aktivierung wird die Abgleichslatenz verringert, aber die Bedingungsprüfungen aller Makros werden insgesamt langsamer."
|
||||
AdvSceneSwitcher.condition.video.usePatternForChangedCheck="Musterabgleich verwenden"
|
||||
AdvSceneSwitcher.condition.video.usePatternForChangedCheck.tooltip="So kann gesteuert werden, wie stark sich das Bild ändern muss, damit die Bedingung erfüllt ist."
|
||||
AdvSceneSwitcher.condition.video.patternThreshold="Schwellenwert: "
|
||||
AdvSceneSwitcher.condition.video.patternThresholdDescription="Ein höherer Schwellenwert bedeutet, dass das Muster stärker an die Videoquelle angepasst werden muss."
|
||||
AdvSceneSwitcher.condition.video.patternThresholdUseAlphaAsMask="Alphakanal als Maske für Muster verwenden."
|
||||
AdvSceneSwitcher.condition.video.brightnessThreshold="Durchschnittliche Helligkeit liegt über:"
|
||||
AdvSceneSwitcher.condition.video.brightnessThresholdDescription="Ein hoher Wert steht für ein helles Bild, ein niedriger Wert für ein dunkles Bild."
|
||||
AdvSceneSwitcher.condition.video.currentBrightness="Aktuelle durchschnittliche Helligkeit: %1"
|
||||
AdvSceneSwitcher.condition.video.objectScaleThreshold="Skalierungsfaktor: "
|
||||
AdvSceneSwitcher.condition.video.objectScaleThresholdDescription="Ein niedrigerer Skalierungsfaktor führt zu mehr Übereinstimmungen, aber zu einer höheren CPU-Belastung."
|
||||
AdvSceneSwitcher.condition.video.minNeighborDescription="Ein höherer Wert für die Mindestanzahl von Nachbarn führt zu weniger, aber qualitativ besseren Übereinstimmungen."
|
||||
AdvSceneSwitcher.condition.video.showMatch="Treffer anzeigen"
|
||||
AdvSceneSwitcher.condition.video.showMatch.loading="Treffer werden gesucht"
|
||||
AdvSceneSwitcher.condition.video.screenshotFail="Screenshot der Quelle konnte nicht erstellt werden!"
|
||||
AdvSceneSwitcher.condition.video.screenshotEmpty="Screenshot ist leer - Ist die Quelle sichtbar?"
|
||||
AdvSceneSwitcher.condition.video.patternMatchFail="Das Muster wurde nicht gefunden!"
|
||||
AdvSceneSwitcher.condition.video.patternMatchSuccess="Das Muster ist rot hervorgehoben."
|
||||
AdvSceneSwitcher.condition.video.objectMatchFail="Objekt wurde nicht gefunden!"
|
||||
AdvSceneSwitcher.condition.video.objectMatchSuccess="Objekt ist rot hervorgehoben."
|
||||
AdvSceneSwitcher.condition.video.modelLoadFail="Modelldaten konnten nicht geladen werden!"
|
||||
AdvSceneSwitcher.condition.video.type.main="OBS's Haupt-Ausgabe"
|
||||
AdvSceneSwitcher.condition.video.type.source="Quelle"
|
||||
AdvSceneSwitcher.condition.video.type.scene="Szene"
|
||||
AdvSceneSwitcher.condition.video.entry="{{videoInputTypes}}{{sources}}{{scenes}}{{condition}}{{imagePath}}"
|
||||
AdvSceneSwitcher.condition.video.entry.modelPath="Modelldaten (Haar-Kaskaden-Klassifikator): {{modelDataPath}}"
|
||||
AdvSceneSwitcher.condition.video.entry.minNeighbor="Minimale Anzahl von Nachbarn: {{minNeighbors}}"
|
||||
AdvSceneSwitcher.condition.video.entry.throttle="{{throttleEnable}}Reduzieren Sie die CPU-Belastung, indem Sie die Prüfung nur alle {{throttleCount}} Millisekunden"
|
||||
AdvSceneSwitcher.condition.video.entry.checkAreaEnable="Kontrolle nur im Bereich durchführen"
|
||||
AdvSceneSwitcher.condition.video.entry.checkArea="{{checkAreaEnable}}{{checkArea}}{{selectArea}}"
|
||||
AdvSceneSwitcher.condition.video.minSize="Minimale Größe:"
|
||||
AdvSceneSwitcher.condition.video.maxSize="Maximale Größe:"
|
||||
AdvSceneSwitcher.condition.video.selectArea="Bereich auswählen"
|
||||
AdvSceneSwitcher.condition.video.selectArea.status="Nur der markierte Bereich wird überprüft"
|
||||
AdvSceneSwitcher.condition.video.width="Breite"
|
||||
AdvSceneSwitcher.condition.video.height="Höhe"
|
||||
AdvSceneSwitcher.condition.stream="Stream"
|
||||
AdvSceneSwitcher.condition.stream.state.start="Stream läuft"
|
||||
AdvSceneSwitcher.condition.stream.state.stop="Stream gestoppt"
|
||||
AdvSceneSwitcher.condition.stream.state.starting="Stream wird gestartet"
|
||||
AdvSceneSwitcher.condition.stream.state.stopping="Stream wird gestoppt"
|
||||
AdvSceneSwitcher.condition.stream.entry="{{streamState}}"
|
||||
AdvSceneSwitcher.condition.record="Aufnahme"
|
||||
AdvSceneSwitcher.condition.record.state.start="Aufnahme läuft"
|
||||
AdvSceneSwitcher.condition.record.state.pause="Aufnahme pausiert"
|
||||
AdvSceneSwitcher.condition.record.state.stop="Aufnahme gestoppt"
|
||||
AdvSceneSwitcher.condition.record.entry="{{recordState}}"
|
||||
AdvSceneSwitcher.condition.process="Prozess"
|
||||
AdvSceneSwitcher.condition.process.entry="{{processes}} läuft {{focused}} und ist fokusiert"
|
||||
AdvSceneSwitcher.condition.process.entry.focus="Aktueller Vordergrundprozess: {{focusProcess}}"
|
||||
AdvSceneSwitcher.condition.idle="Leerlauf"
|
||||
AdvSceneSwitcher.condition.idle.entry="Keine Tastatur- oder Mauseingaben für {{duration}}"
|
||||
AdvSceneSwitcher.condition.pluginState="Plugin-Status"
|
||||
AdvSceneSwitcher.condition.pluginState.state.start="Plugin gestartet"
|
||||
AdvSceneSwitcher.condition.pluginState.state.restart="Plugin neu gestartet"
|
||||
AdvSceneSwitcher.condition.pluginState.state.running="Plugin läuft"
|
||||
AdvSceneSwitcher.condition.pluginState.state.shutdown="OBS wird runtergefahren"
|
||||
AdvSceneSwitcher.condition.pluginState.state.sceneCollection="Szenensammlung geladen"
|
||||
AdvSceneSwitcher.condition.pluginState.state.sceneSwitched="Szene durch vorheriges Makro geändert"
|
||||
AdvSceneSwitcher.condition.pluginState.entry="{{condition}}"
|
||||
AdvSceneSwitcher.condition.timer="Timer"
|
||||
AdvSceneSwitcher.condition.timer.type.fixed="Fixe"
|
||||
AdvSceneSwitcher.condition.timer.type.random="Zufällige"
|
||||
AdvSceneSwitcher.condition.timer.pause="Pausieren"
|
||||
AdvSceneSwitcher.condition.timer.continue="Fortfahren"
|
||||
AdvSceneSwitcher.condition.timer.entry.line1.fixed="{{type}} Dauer von {{duration}} ist abgelaufen"
|
||||
AdvSceneSwitcher.condition.timer.entry.line1.random="{{type}} Dauer von {{duration}} bis {{duration2}} ist abgelaufen"
|
||||
AdvSceneSwitcher.condition.timer.entry.line2="Restliche Zeit: {{remaining}} Sekunden"
|
||||
AdvSceneSwitcher.condition.timer.entry.line3="{{pauseContinue}} {{reset}} {{saveRemaining}} Verbleibende Zeit speichern {{autoReset}} Automatisches Zurücksetzen des Timers nach Erreichen der Dauer"
|
||||
AdvSceneSwitcher.condition.timer.reset="Zurücksetzen"
|
||||
AdvSceneSwitcher.condition.macro="Makro"
|
||||
AdvSceneSwitcher.condition.macro.type.count="Zähler"
|
||||
AdvSceneSwitcher.condition.macro.type.state="Zustand"
|
||||
AdvSceneSwitcher.condition.macro.type.multiState="Mehrere Zustände"
|
||||
AdvSceneSwitcher.condition.macro.type.selection="Bedingungs-Typ: {{types}}"
|
||||
AdvSceneSwitcher.condition.macro.count.type.below="Weniger als"
|
||||
AdvSceneSwitcher.condition.macro.count.type.above="Mehr als"
|
||||
AdvSceneSwitcher.condition.macro.count.type.equal="Exakt"
|
||||
AdvSceneSwitcher.condition.macro.state.type.below="Weniger als"
|
||||
AdvSceneSwitcher.condition.macro.state.type.above="Mehr als"
|
||||
AdvSceneSwitcher.condition.macro.state.type.equal="Exakt"
|
||||
AdvSceneSwitcher.condition.macro.count.reset="Zurücksetzen"
|
||||
AdvSceneSwitcher.condition.macro.pausedWarning="Ausgewähltes Makro ist derzeit pausiert!"
|
||||
AdvSceneSwitcher.condition.macro.state.entry="Bedingungen für {{macros}} sind wahr"
|
||||
AdvSceneSwitcher.condition.macro.multistate.entry="Bedingungen für {{multiStateConditions}}{{multiStateCount}} der folgenden Makros wahr sind:"
|
||||
AdvSceneSwitcher.condition.macro.count.entry.line1="{{macros}} wurde ausgeführt {{conditions}} {{count}} Mal"
|
||||
AdvSceneSwitcher.condition.macro.count.entry.line2="Aktuelle Anzahl: {{currentCount}} {{resetCount}}"
|
||||
AdvSceneSwitcher.condition.source="Quelle"
|
||||
AdvSceneSwitcher.condition.source.type.active="Ist aktiv"
|
||||
AdvSceneSwitcher.condition.source.type.showing="Wird angezeigt"
|
||||
AdvSceneSwitcher.condition.source.type.settings="Einstellungen stimmen überein"
|
||||
AdvSceneSwitcher.condition.source.getSettings="Aktuelle Einstellungen abfragen"
|
||||
AdvSceneSwitcher.condition.source.entry.line1="{{sources}} {{conditions}}"
|
||||
AdvSceneSwitcher.condition.source.entry.line2="{{settings}}"
|
||||
AdvSceneSwitcher.condition.source.entry.line3="{{regex}} {{getSettings}}"
|
||||
AdvSceneSwitcher.condition.virtualCamera="Virtuelle Kamera"
|
||||
AdvSceneSwitcher.condition.virtualCamera.state.start="Virtuelle Kamera gestartet"
|
||||
AdvSceneSwitcher.condition.virtualCamera.state.stop="Virtuelle Kamera gestoppt"
|
||||
AdvSceneSwitcher.condition.virtualCamera.entry="{{states}}"
|
||||
AdvSceneSwitcher.condition.filter="Filter"
|
||||
AdvSceneSwitcher.condition.filter.type.active="Ist aktiviert"
|
||||
AdvSceneSwitcher.condition.filter.type.showing="Ist deaktiviert"
|
||||
AdvSceneSwitcher.condition.filter.type.settings="Einstellungen passen"
|
||||
AdvSceneSwitcher.condition.filter.getSettings="Aktuelle Einstellungen abrufen"
|
||||
AdvSceneSwitcher.condition.filter.entry.line1="Auf {{sources}} {{filters}} {{conditions}}"
|
||||
AdvSceneSwitcher.condition.filter.entry.line2="{{settings}}"
|
||||
AdvSceneSwitcher.condition.filter.entry.line3="{{regex}} {{getSettings}}"
|
||||
AdvSceneSwitcher.condition.sceneOrder="Reihenfolge der Szenenelemente"
|
||||
AdvSceneSwitcher.condition.sceneOrder.type.above="Ist über"
|
||||
AdvSceneSwitcher.condition.sceneOrder.type.below="Ist unter"
|
||||
AdvSceneSwitcher.condition.sceneOrder.type.position="Ist in Position"
|
||||
AdvSceneSwitcher.condition.sceneOrder.positionInfo="Der Positionswert beginnt ganz unten bei 0 und erhöht sich für jedes Szenenelement, auch in Szenengruppen, um eins"
|
||||
AdvSceneSwitcher.condition.sceneOrder.entry="Auf{{scenes}}{{sources}}{{conditions}}{{sources2}}{{position}}"
|
||||
AdvSceneSwitcher.condition.hotkey="Hotkey"
|
||||
AdvSceneSwitcher.condition.hotkey.name="Makro-Trigger-Hotkey"
|
||||
AdvSceneSwitcher.condition.hotkey.tip="Hinweis: Die Tastenkombinationen für diesen Hotkey können in den OBS-Einstellungen konfiguriert werden"
|
||||
AdvSceneSwitcher.condition.hotkey.entry.line1="Hotkey ist gedrückt"
|
||||
AdvSceneSwitcher.condition.hotkey.entry.line2="Name: {{name}}"
|
||||
AdvSceneSwitcher.condition.replay="Replay Buffer"
|
||||
AdvSceneSwitcher.condition.replay.state.stopped="Replay Buffer gestoppt"
|
||||
AdvSceneSwitcher.condition.replay.state.started="Replay Buffer gestartet"
|
||||
AdvSceneSwitcher.condition.replay.state.saved="Replay Buffer gespeichert"
|
||||
AdvSceneSwitcher.condition.replay.entry="{{state}}"
|
||||
AdvSceneSwitcher.condition.date="Datum"
|
||||
AdvSceneSwitcher.condition.date.anyDay="Beliebiger Tag"
|
||||
AdvSceneSwitcher.condition.date.monday="Montag"
|
||||
AdvSceneSwitcher.condition.date.tuesday="Dienstag"
|
||||
AdvSceneSwitcher.condition.date.wednesday="Mittwoch"
|
||||
AdvSceneSwitcher.condition.date.thursday="Donnerstag"
|
||||
AdvSceneSwitcher.condition.date.friday="Freitag"
|
||||
AdvSceneSwitcher.condition.date.saturday="Samstag"
|
||||
AdvSceneSwitcher.condition.date.sunday="Sonntag"
|
||||
AdvSceneSwitcher.condition.date.state.at="Am"
|
||||
AdvSceneSwitcher.condition.date.state.after="Nach"
|
||||
AdvSceneSwitcher.condition.date.state.before="Vor"
|
||||
AdvSceneSwitcher.condition.date.state.between="Zwischen"
|
||||
AdvSceneSwitcher.condition.date.state.pattern="Muster"
|
||||
AdvSceneSwitcher.condition.date.separator="und"
|
||||
AdvSceneSwitcher.condition.date.ignoreDate="Wenn diese Option nicht aktiviert ist, wird die Datumskomponente ignoriert"
|
||||
AdvSceneSwitcher.condition.date.ignoreTime="Wenn diese Option nicht aktiviert ist, wird die Zeitkomponente ignoriert"
|
||||
AdvSceneSwitcher.condition.date.showAdvancedSettings="Erweiterte Einstellungen anzeigen"
|
||||
AdvSceneSwitcher.condition.date.showSimpleSettings="Einfache Einstellungen anzeigen"
|
||||
AdvSceneSwitcher.condition.date.entry.simple="Am {{dayOfWeek}} {{weekCondition}} {{ignoreWeekTime}}{{weekTime}}"
|
||||
AdvSceneSwitcher.condition.date.entry.advanced="{{condition}} {{ignoreDate}}{{date}} {{ignoreTime}}{{time}} {{separator}} {{date2}} {{time2}}"
|
||||
AdvSceneSwitcher.condition.date.entry.repeat="{{repeat}} Wiederholen alle {{duration}} bei Datumsübereinstimmung"
|
||||
AdvSceneSwitcher.condition.date.entry.pattern="Aktuelles Datum \"{{currentDate}}\" entspricht dem Muster {{pattern}}"
|
||||
AdvSceneSwitcher.condition.date.entry.nextMatchDate="Nächster Treffer bei: %1"
|
||||
AdvSceneSwitcher.condition.date.entry.updateOnRepeat="{{updateOnRepeat}} Bei Wiederholung ausgewähltes Datum auf Wiederholungsdatum aktualisieren"
|
||||
AdvSceneSwitcher.condition.sceneTransform="Szenenelement transformieren"
|
||||
AdvSceneSwitcher.condition.sceneTransform.getTransform="Transformation erhalten"
|
||||
AdvSceneSwitcher.condition.sceneTransform.entry.line1="Auf{{scenes}}{{sources}}passt zur Transformation"
|
||||
AdvSceneSwitcher.condition.sceneTransform.entry.line2="{{settings}}"
|
||||
AdvSceneSwitcher.condition.sceneTransform.entry.line3="{{regex}} {{getSettings}}"
|
||||
AdvSceneSwitcher.condition.transition="Übergang"
|
||||
AdvSceneSwitcher.condition.transition.type.current="Aktueller Übergangstyp ist"
|
||||
AdvSceneSwitcher.condition.transition.type.duration="Aktuelle Übergangsdauer beträgt"
|
||||
AdvSceneSwitcher.condition.transition.type.started="Übergang gestartet"
|
||||
AdvSceneSwitcher.condition.transition.type.ended="Übergang beendet"
|
||||
AdvSceneSwitcher.condition.transition.type.transitionSource="Übergang von"
|
||||
AdvSceneSwitcher.condition.transition.type.transitionTarget="Übergang zu"
|
||||
AdvSceneSwitcher.condition.transition.durationSuffix="Sekunden"
|
||||
AdvSceneSwitcher.condition.transition.entry="{{conditions}}{{transitions}}{{scenes}}{{duration}}{{durationSuffix}}"
|
||||
AdvSceneSwitcher.condition.sceneVisibility="Sichtbarkeit von Szenenelementen"
|
||||
AdvSceneSwitcher.condition.sceneVisibility.type.shown="ist sichtbar"
|
||||
AdvSceneSwitcher.condition.sceneVisibility.type.hidden="ist versteckt"
|
||||
AdvSceneSwitcher.condition.sceneVisibility.type.changed="Sichtbarkeit verändert"
|
||||
AdvSceneSwitcher.condition.sceneVisibility.entry="Auf{{scenes}}{{sources}}{{conditions}}"
|
||||
AdvSceneSwitcher.condition.studioMode="Studio-Modus"
|
||||
AdvSceneSwitcher.condition.studioMode.state.active="Studio-Modus ist aktiviert"
|
||||
AdvSceneSwitcher.condition.studioMode.state.notActive="Studio-Modus ist nicht aktiviert"
|
||||
AdvSceneSwitcher.condition.studioMode.state.previewScene="Vorschau-Szene ist"
|
||||
AdvSceneSwitcher.condition.studioMode.entry="{{conditions}}{{scenes}}"
|
||||
AdvSceneSwitcher.condition.openvr="OpenVR"
|
||||
AdvSceneSwitcher.condition.openvr.errorStatus="OpenVR Fehler: "
|
||||
AdvSceneSwitcher.condition.openvr.entry.line1="HMD ist in ..."
|
||||
AdvSceneSwitcher.condition.openvr.entry.line2="{{controls}}"
|
||||
AdvSceneSwitcher.condition.openvr.entry.line3="HMD ist derzeit bei {{xPos}} x {{yPos}} x {{zPos}}"
|
||||
AdvSceneSwitcher.condition.stats="OBS-Statistiken"
|
||||
AdvSceneSwitcher.condition.stats.type.fps="FPS"
|
||||
AdvSceneSwitcher.condition.stats.type.CPUUsage="CPU-Auslastung"
|
||||
AdvSceneSwitcher.condition.stats.type.HDDSpaceAvailable="Verfügbarer Speicherplatz"
|
||||
AdvSceneSwitcher.condition.stats.type.memoryUsage="Speicherverbrauch"
|
||||
AdvSceneSwitcher.condition.stats.type.averageTimeToRender="Durchschnittliche Zeit zum Rendern eines Frames"
|
||||
AdvSceneSwitcher.condition.stats.type.skippedFrames="Übersprungene Frames aufgrund der Kodierungsverzögerung"
|
||||
AdvSceneSwitcher.condition.stats.type.missedFrames="Verpasste Frames durch Rendering-Verzögerung"
|
||||
AdvSceneSwitcher.condition.stats.type.droppedFrames.stream="Verlorene Frames im Stream"
|
||||
AdvSceneSwitcher.condition.stats.type.megabytesSent.stream="Gesamtdatenmenge des Streams"
|
||||
AdvSceneSwitcher.condition.stats.type.bitrate.stream="Stream-Bitrate"
|
||||
AdvSceneSwitcher.condition.stats.type.droppedFrames.recording="Verlorene Frames bei der Aufnahme"
|
||||
AdvSceneSwitcher.condition.stats.type.megabytesSent.recording="Gesamtdatenmenge der Aufnahme"
|
||||
AdvSceneSwitcher.condition.stats.type.bitrate.recording="Aufnahme-Bitrate"
|
||||
AdvSceneSwitcher.condition.stats.condition.above="über"
|
||||
AdvSceneSwitcher.condition.stats.condition.equals="gleich"
|
||||
AdvSceneSwitcher.condition.stats.condition.below="unter"
|
||||
AdvSceneSwitcher.condition.stats.dockHint="Das Dock \"Stats\" öffnen, um den aktuellen Status anzuzeigen"
|
||||
AdvSceneSwitcher.condition.stats.entry="{{stats}} ist {{condition}} {{value}}"
|
||||
AdvSceneSwitcher.condition.profile="Profil"
|
||||
AdvSceneSwitcher.condition.profile.entry="Derzeit aktives Profil ist {{profiles}}"
|
||||
AdvSceneSwitcher.condition.websocket="Websocket"
|
||||
AdvSceneSwitcher.condition.websocket.type.request="Anfrage"
|
||||
AdvSceneSwitcher.condition.websocket.type.event="Ereignis"
|
||||
AdvSceneSwitcher.condition.websocket.useRegex="Reguläre Ausdrücke verwenden"
|
||||
AdvSceneSwitcher.condition.websocket.entry.request="{{type}} wurde empfangen:"
|
||||
AdvSceneSwitcher.condition.websocket.entry.event="{{type}} wurde empfangen von {{connection}}:"
|
||||
AdvSceneSwitcher.condition.variable="Variabel"
|
||||
AdvSceneSwitcher.condition.variable.type.compare="entspricht"
|
||||
AdvSceneSwitcher.condition.variable.type.empty="ist leer"
|
||||
AdvSceneSwitcher.condition.variable.type.number="ist eine Zahl"
|
||||
AdvSceneSwitcher.condition.variable.type.lessThan="ist kleiner als"
|
||||
AdvSceneSwitcher.condition.variable.type.greaterThan="ist größer als"
|
||||
AdvSceneSwitcher.condition.variable.type.valueChanged="Wert verändert"
|
||||
AdvSceneSwitcher.condition.variable.type.equalsVariable="entspricht Variable"
|
||||
AdvSceneSwitcher.condition.variable.type.lessThanVariable="ist kleiner als die Variable"
|
||||
AdvSceneSwitcher.condition.variable.type.greaterThanVariable="ist größer als die Variable"
|
||||
AdvSceneSwitcher.condition.variable.entry="{{variables}}{{conditions}}{{strValue}}{{numValue}}{{variables2}}"
|
||||
|
||||
; Macro Actions
|
||||
AdvSceneSwitcher.action.switchScene="Szene wechseln"
|
||||
AdvSceneSwitcher.action.scene.entry="Wechsle zu Szene{{scenes}}mittels{{transitions}}mit einer Dauer von{{duration}}Sekunden"
|
||||
AdvSceneSwitcher.action.scene.entry.noDuration="Wechsle zu Szene{{scenes}}mittels{{transitions}}"
|
||||
AdvSceneSwitcher.action.scene.blockUntilTransitionDone="Warten, bis der Übergang zur Zielszene abgeschlossen ist"
|
||||
AdvSceneSwitcher.action.wait="Warten"
|
||||
AdvSceneSwitcher.action.wait.type.fixed="fixe"
|
||||
AdvSceneSwitcher.action.wait.type.random="zufällig"
|
||||
AdvSceneSwitcher.action.wait.entry.fixed="Warte für {{waitType}} Dauer von {{duration}}"
|
||||
AdvSceneSwitcher.action.wait.entry.random="Warte für {{waitType}} Dauer von {{duration}} bis {{duration2}}"
|
||||
AdvSceneSwitcher.action.audio="Audio"
|
||||
AdvSceneSwitcher.action.audio.type.mute="Stummgeschaltet"
|
||||
AdvSceneSwitcher.action.audio.type.unmute="Nicht stummgeschaltet"
|
||||
AdvSceneSwitcher.action.audio.type.sourceVolume="Quell-Lautstärke einstellen"
|
||||
AdvSceneSwitcher.action.audio.type.masterVolume="Master-Lautstärke einstellen"
|
||||
AdvSceneSwitcher.action.audio.type.syncOffset="Sync-Offset einstellen"
|
||||
AdvSceneSwitcher.action.audio.type.monitor="Audio-Monitoring einstellen"
|
||||
AdvSceneSwitcher.action.audio.type.balance="Audio-Balance einstellen"
|
||||
AdvSceneSwitcher.action.audio.balance.description="Den Schieberegler in die Richtung des Audiokanals drehen, auf den fokussiert werden soll."
|
||||
AdvSceneSwitcher.action.audio.fade.type.duration="über eine Dauer von"
|
||||
AdvSceneSwitcher.action.audio.fade.type.rate="mit einer Rate von"
|
||||
AdvSceneSwitcher.action.audio.fade.duration="{{fade}}Blende {{fadeTypes}} {{duration}} Sekunden."
|
||||
AdvSceneSwitcher.action.audio.fade.rate="{{fade}}Blende {{fadeTypes}} {{rate}}pro Sekunde."
|
||||
AdvSceneSwitcher.action.audio.fade.wait="Warten, bis die Blende abgeschlossen ist."
|
||||
AdvSceneSwitcher.action.audio.fade.abort="Abbruch einer bereits aktiven Blende."
|
||||
AdvSceneSwitcher.action.audio.entry="{{actions}}{{audioSources}}{{volume}}{{syncOffset}}{{monitorTypes}}"
|
||||
AdvSceneSwitcher.action.recording="Aufnahme"
|
||||
AdvSceneSwitcher.action.recording.type.stop="Aufnahme stoppen"
|
||||
AdvSceneSwitcher.action.recording.type.start="Aufnahme starten"
|
||||
AdvSceneSwitcher.action.recording.type.pause="Aufnahme pausieren"
|
||||
AdvSceneSwitcher.action.recording.type.unpause="Aufnahme nicht mehr pausieren"
|
||||
AdvSceneSwitcher.action.recording.type.split="Aufnahme-Datei teilen"
|
||||
AdvSceneSwitcher.action.recording.pause.hint="Bitte beachten, dass je nach Aufnahmeeinstellung die Aufnahme möglicherweise nicht unterbrochen werden kann"
|
||||
AdvSceneSwitcher.action.recording.split.hint="Vergewissern, dass die automatische Dateiaufteilung in den OBS-Einstellungen aktiviert ist!"
|
||||
AdvSceneSwitcher.action.recording.entry="{{actions}}{{pauseHint}}{{splitHint}}"
|
||||
AdvSceneSwitcher.action.replay="Replay Buffer"
|
||||
AdvSceneSwitcher.action.replay.saveWarn="Warnung: Ein zu häufiges Speichern kann dazu führen, dass der Replay Buffer nicht gespeichert wird!"
|
||||
AdvSceneSwitcher.action.replay.type.stop="Replay Buffer stoppen"
|
||||
AdvSceneSwitcher.action.replay.type.start="Replay Buffer starten"
|
||||
AdvSceneSwitcher.action.replay.type.save="Replay Buffer speichern"
|
||||
AdvSceneSwitcher.action.replay.entry="{{actions}}"
|
||||
AdvSceneSwitcher.action.streaming="Stream"
|
||||
AdvSceneSwitcher.action.streaming.type.stop="Stream stoppen"
|
||||
AdvSceneSwitcher.action.streaming.type.start="Stream starten"
|
||||
AdvSceneSwitcher.action.streaming.entry="{{actions}}"
|
||||
AdvSceneSwitcher.action.run="Ausführen"
|
||||
AdvSceneSwitcher.action.run.arguments="Argumente:"
|
||||
AdvSceneSwitcher.action.run.addArgument="Argument hinzufügen"
|
||||
AdvSceneSwitcher.action.run.addArgumentDescription="Neues Argument hinzufügen:"
|
||||
AdvSceneSwitcher.action.run.entry="Ausführen {{filePath}}"
|
||||
AdvSceneSwitcher.action.run.entry.workingDirectory="Arbeitsverzeichnis:{{workingDirectory}}"
|
||||
AdvSceneSwitcher.action.sceneVisibility="Sichtbarkeit von Szenenelementen"
|
||||
AdvSceneSwitcher.action.sceneVisibility.type.show="Anzeigen"
|
||||
AdvSceneSwitcher.action.sceneVisibility.type.hide="Verstecken"
|
||||
AdvSceneSwitcher.action.sceneVisibility.type.toggle="Umschalten"
|
||||
AdvSceneSwitcher.action.sceneVisibility.type.source="Quelle"
|
||||
AdvSceneSwitcher.action.sceneVisibility.type.sourceGroup="Beliebig"
|
||||
AdvSceneSwitcher.action.sceneVisibility.entry="Auf{{scenes}}{{actions}}{{sourceTypes}}{{sources}}{{sourceGroups}}"
|
||||
AdvSceneSwitcher.action.filter="Filter"
|
||||
AdvSceneSwitcher.action.filter.type.enable="Aktivieren"
|
||||
AdvSceneSwitcher.action.filter.type.disable="Deaktivieren"
|
||||
AdvSceneSwitcher.action.filter.type.settings="Einstellungen festlegen"
|
||||
AdvSceneSwitcher.action.filter.entry="Auf {{sources}} {{actions}} {{filters}}"
|
||||
AdvSceneSwitcher.action.filter.getSettings="Aktuelle Einstellungen abfragen"
|
||||
AdvSceneSwitcher.action.source="Quelle"
|
||||
AdvSceneSwitcher.action.source.type.enable="Aktivieren"
|
||||
AdvSceneSwitcher.action.source.type.disable="Deaktivieren"
|
||||
AdvSceneSwitcher.action.source.type.settings="Einstellungen festlegen"
|
||||
AdvSceneSwitcher.action.source.type.refreshSettings="Aktualisieren der Quelleneinstellungen"
|
||||
AdvSceneSwitcher.action.source.type.pressSettingsButton="Drücken der Einstellungstaste"
|
||||
AdvSceneSwitcher.action.source.type.refreshSettings.tooltip="Kann verwendet werden, um Browser-, Medien- usw. Quellen zu aktualisieren"
|
||||
AdvSceneSwitcher.action.source.noSettingsButtons="Keine Buttons gefunden!"
|
||||
AdvSceneSwitcher.action.source.entry="{{actions}}{{sources}}{{settingsButtons}}"
|
||||
AdvSceneSwitcher.action.source.warning="Warnung: Das globale Aktivieren und Deaktivieren von Quellen kann nicht über die OBS-Benutzeroberfläche gesteuert werden"
|
||||
AdvSceneSwitcher.action.source.getSettings="Aktuelle Einstellungen abfragen"
|
||||
AdvSceneSwitcher.action.media="Medien"
|
||||
AdvSceneSwitcher.action.media.type.play="Abspielen"
|
||||
AdvSceneSwitcher.action.media.type.pause="Pausieren"
|
||||
AdvSceneSwitcher.action.media.type.stop="Stoppen"
|
||||
AdvSceneSwitcher.action.media.type.restart="Neu starten"
|
||||
AdvSceneSwitcher.action.media.type.next="Nächste"
|
||||
AdvSceneSwitcher.action.media.type.previous="Vorherige"
|
||||
AdvSceneSwitcher.action.media.type.seek="Suche nach"
|
||||
AdvSceneSwitcher.action.media.entry="{{actions}}{{duration}}{{mediaSources}}"
|
||||
AdvSceneSwitcher.action.macro="Makro"
|
||||
AdvSceneSwitcher.action.macro.type.pause="Pausieren"
|
||||
AdvSceneSwitcher.action.macro.type.unpause="Nicht mehr pausieren"
|
||||
AdvSceneSwitcher.action.macro.type.resetCounter="Zähler zurücksetzen"
|
||||
AdvSceneSwitcher.action.macro.type.run="Aktionen ausführen"
|
||||
AdvSceneSwitcher.action.macro.type.stop="Aktionen stoppen"
|
||||
AdvSceneSwitcher.action.macro.entry="{{actions}} {{macros}}"
|
||||
AdvSceneSwitcher.action.pluginState="Plugin-Status"
|
||||
AdvSceneSwitcher.action.pluginState.type.stop="Erweiterten Automatischen Szenenwechsler stoppen"
|
||||
AdvSceneSwitcher.action.pluginState.type.noMatch="Ändern des Nichtübereinstimmungsverhaltens:"
|
||||
AdvSceneSwitcher.action.pluginState.type.import="Einstellungen importieren aus"
|
||||
AdvSceneSwitcher.action.pluginState.importWarning="Hinweis: Die Aktion wird ignoriert, solange das Einstellungsfenster geöffnet ist."
|
||||
AdvSceneSwitcher.action.pluginState.entry="{{actions}}{{values}}{{scenes}}{{settings}}{{settingsWarning}}"
|
||||
AdvSceneSwitcher.action.virtualCamera="Virtuelle Kamera"
|
||||
AdvSceneSwitcher.action.virtualCamera.type.stop="Virtuelle Kamera stoppen"
|
||||
AdvSceneSwitcher.action.virtualCamera.type.start="Virtuelle Kamera starten"
|
||||
AdvSceneSwitcher.action.virtualCamera.entry="{{actions}}"
|
||||
AdvSceneSwitcher.action.hotkey="Hotkey"
|
||||
AdvSceneSwitcher.action.hotkey.leftShift="Linke Umschalttaste"
|
||||
AdvSceneSwitcher.action.hotkey.rightShift="Rechte Umschalttaste"
|
||||
AdvSceneSwitcher.action.hotkey.leftCtrl="Linke Strg-Taste"
|
||||
AdvSceneSwitcher.action.hotkey.rightCtrl="Rechte Strg-Taste"
|
||||
AdvSceneSwitcher.action.hotkey.leftAlt="Linke Alt-Taste"
|
||||
AdvSceneSwitcher.action.hotkey.rightAlt="Rechte Alt-Taste"
|
||||
AdvSceneSwitcher.action.hotkey.leftMeta="Linke Meta-Taste"
|
||||
AdvSceneSwitcher.action.hotkey.rightMeta="Rechte Meta-Taste"
|
||||
AdvSceneSwitcher.action.hotkey.onlyOBS="Tastendruck nur an OBS senden"
|
||||
AdvSceneSwitcher.action.hotkey.disabled="Globale Tastendrücke können nicht simuliert werden - die Funktionalität beschränkt sich auf das Senden von Tastendrücken an OBS!"
|
||||
AdvSceneSwitcher.action.hotkey.entry="Drücke {{keys}} für {{duration}} Millisekunden"
|
||||
AdvSceneSwitcher.action.sceneOrder="Reihenfolge der Szenenelemente"
|
||||
AdvSceneSwitcher.action.sceneOrder.type.moveUp="Nach oben verschieben"
|
||||
AdvSceneSwitcher.action.sceneOrder.type.moveDown="Nach unten verschieben"
|
||||
AdvSceneSwitcher.action.sceneOrder.type.moveTop="An erste Stelle verschieben"
|
||||
AdvSceneSwitcher.action.sceneOrder.type.moveBottom="An letzte Stelle verschieben"
|
||||
AdvSceneSwitcher.action.sceneOrder.type.movePosition="An Position verschieben"
|
||||
AdvSceneSwitcher.action.sceneOrder.entry="Auf{{scenes}}{{actions}}{{sources}}{{position}}"
|
||||
AdvSceneSwitcher.action.sceneTransform="Szenenelement transformieren"
|
||||
AdvSceneSwitcher.action.sceneTransform.getTransform="Transformation erhalten"
|
||||
AdvSceneSwitcher.action.sceneTransform.entry="Auf{{scenes}}transformieren{{sources}}"
|
||||
AdvSceneSwitcher.action.file="Datei"
|
||||
AdvSceneSwitcher.action.file.type.write="Schreiben"
|
||||
AdvSceneSwitcher.action.file.type.append="Anhängen"
|
||||
AdvSceneSwitcher.action.file.entry="{{actions}} in {{filePath}}:"
|
||||
AdvSceneSwitcher.action.studioMode="Studio-Modus"
|
||||
AdvSceneSwitcher.action.studioMode.type.swap="Vorschau- und Programm-Szene tauschen"
|
||||
AdvSceneSwitcher.action.studioMode.type.setScene="Vorschau-Szene einstellen auf"
|
||||
AdvSceneSwitcher.action.studioMode.type.enable="Studio-Modus aktivieren"
|
||||
AdvSceneSwitcher.action.studioMode.type.disable="Studio-Modus deaktivieren"
|
||||
AdvSceneSwitcher.action.studioMode.entry="{{actions}}{{scenes}}"
|
||||
AdvSceneSwitcher.action.transition="Übergang"
|
||||
AdvSceneSwitcher.action.transition.type.scene="Szenenübergang"
|
||||
AdvSceneSwitcher.action.transition.type.sceneOverride="Szenenübergang überschreiben"
|
||||
AdvSceneSwitcher.action.transition.type.sourceShow="Übergang der Quelle anzeigen"
|
||||
AdvSceneSwitcher.action.transition.type.sourceHide="Übergang der Quelle verstecken"
|
||||
AdvSceneSwitcher.action.transition.entry.line1="Anpassen {{type}}{{scenes}}{{sources}}"
|
||||
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Übergangstyp festlegen auf {{transitions}}"
|
||||
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Übergangsdauer festlegen auf {{duration}}Sekunden"
|
||||
AdvSceneSwitcher.action.timer="Timer"
|
||||
AdvSceneSwitcher.action.timer.type.pause="Pausieren"
|
||||
AdvSceneSwitcher.action.timer.type.continue="Fortsetzen"
|
||||
AdvSceneSwitcher.action.timer.type.reset="Zurücksetzen"
|
||||
AdvSceneSwitcher.action.timer.type.setTimeRemaining="Verbleibende Zeit einstellen der"
|
||||
AdvSceneSwitcher.action.timer.entry="{{timerAction}} Timer von {{macros}} {{duration}}"
|
||||
AdvSceneSwitcher.action.random="Zufall"
|
||||
AdvSceneSwitcher.action.random.allowRepeat="Aufeinanderfolgende Ausführung desselben Makros zulassen"
|
||||
AdvSceneSwitcher.action.random.entry="Zufällige Ausführung eines der folgenden Makros (pausierte Makros werden ignoriert)"
|
||||
AdvSceneSwitcher.action.systray="Benachrichtigung im System-Tray"
|
||||
AdvSceneSwitcher.action.systray.entry="Benachrichtigung anzeigen: {{message}}"
|
||||
AdvSceneSwitcher.action.screenshot="Screenshot"
|
||||
AdvSceneSwitcher.action.screenshot.save.default="Standard"
|
||||
AdvSceneSwitcher.action.screenshot.save.custom="Benutzerdefiniert"
|
||||
AdvSceneSwitcher.action.screenshot.type.source="Quelle"
|
||||
AdvSceneSwitcher.action.screenshot.type.scene="Szene"
|
||||
AdvSceneSwitcher.action.screenshot.mainOutput="OBS's Haupt-Ausgabe"
|
||||
AdvSceneSwitcher.action.screenshot.blackscreenNote="Quellen oder Szenen, die nicht immer gerendert werden, können dazu führen, dass einige Teile der Screenshots leer bleiben."
|
||||
AdvSceneSwitcher.action.screenshot.entry="Screenshot{{targetType}}{{sources}}{{scenes}}und speichere in{{saveType}}Pfad"
|
||||
AdvSceneSwitcher.action.profile="Profil"
|
||||
AdvSceneSwitcher.action.profile.entry="Aktives Profil umschalten auf {{profiles}}"
|
||||
AdvSceneSwitcher.action.sceneCollection="Szenensammlung"
|
||||
AdvSceneSwitcher.action.sceneCollection.entry="Aktive Szenensammlung umschalten auf {{sceneCollections}}"
|
||||
AdvSceneSwitcher.action.sceneCollection.warning="Hinweis: Alle danach folgenden Aktionen werden nicht ausgeführt, da durch die Änderung der Szenensammlung auch die Einstellungen des Szenenwechslers neu geladen werden.\nDie Szenensammlungsaktion wird ignoriert, solange das Einstellungsfenster geöffnet ist."
|
||||
AdvSceneSwitcher.action.sequence="Sequenz"
|
||||
AdvSceneSwitcher.action.sequence.entry="Jedes Mal, wenn diese Aktion ausgeführt wird, wird das nächste Makro in der Liste ausgeführt (pausierte Makros werden ignoriert)"
|
||||
AdvSceneSwitcher.action.sequence.status="Zuletzt ausgeführtes Makro: %1 - Nächstes auszuführendes Makro: %2"
|
||||
AdvSceneSwitcher.action.sequence.status.none="keines"
|
||||
AdvSceneSwitcher.action.sequence.restart="Neustart am Anfang, wenn das Ende der Liste erreicht ist"
|
||||
AdvSceneSwitcher.action.sequence.continueFrom="Weiter mit ausgewähltem Element"
|
||||
AdvSceneSwitcher.action.websocket="Websocket"
|
||||
AdvSceneSwitcher.action.websocket.type.request="Anfrage"
|
||||
AdvSceneSwitcher.action.websocket.type.event="Ereignis"
|
||||
AdvSceneSwitcher.action.websocket.entry.request="Szenenwechsler {{type}} via {{connection}} senden"
|
||||
AdvSceneSwitcher.action.websocket.entry.event="Szenenwechsler {{type}} an verbundene Clients senden"
|
||||
AdvSceneSwitcher.action.http="HTTP"
|
||||
AdvSceneSwitcher.action.http.type.get="GET"
|
||||
AdvSceneSwitcher.action.http.type.post="POST"
|
||||
AdvSceneSwitcher.action.http.entry.line1="Sende {{method}} an {{url}}"
|
||||
AdvSceneSwitcher.action.http.entry.line2="Timeout: {{timeout}} Sekunden"
|
||||
AdvSceneSwitcher.action.variable="Variabel"
|
||||
AdvSceneSwitcher.action.variable.type.set="Auf festen Wert einstellen"
|
||||
AdvSceneSwitcher.action.variable.type.append="Anhängen"
|
||||
AdvSceneSwitcher.action.variable.type.appendVar="Variable anhängen"
|
||||
AdvSceneSwitcher.action.variable.type.increment="Erhöhen"
|
||||
AdvSceneSwitcher.action.variable.type.decrement="Verringern"
|
||||
AdvSceneSwitcher.action.variable.type.setActionValue="Auf Aktionswert setzen"
|
||||
AdvSceneSwitcher.action.variable.type.setConditionValue="Auf Bedingungswert setzen"
|
||||
AdvSceneSwitcher.action.variable.invalidSelection="Ungültige Auswahl!"
|
||||
AdvSceneSwitcher.action.variable.actionNoVariableSupport="Das Abrufen von Variablenwerten aus %1 Aktionen wird nicht unterstützt!"
|
||||
AdvSceneSwitcher.action.variable.conditionNoVariableSupport="Das Abrufen von Variablenwerten aus %1 Bedingungen wird nicht unterstützt!"
|
||||
AdvSceneSwitcher.action.variable.currentSegmentValue="Aktueller Wert:"
|
||||
AdvSceneSwitcher.action.variable.entry="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}"
|
||||
|
||||
|
||||
; Transition Tab
|
||||
AdvSceneSwitcher.transitionTab.title="Szenenübergänge"
|
||||
AdvSceneSwitcher.transitionTab.transitionForAToB="Szenenübergänge für automatisierte Szenenwechsel von Szene A zu Szene B"
|
||||
@@ -117,6 +651,7 @@ AdvSceneSwitcher.mediaTab.states.Paused="Pausiert"
|
||||
AdvSceneSwitcher.mediaTab.states.stopped="Gestoppt"
|
||||
AdvSceneSwitcher.mediaTab.states.ended="Beendet"
|
||||
AdvSceneSwitcher.mediaTab.states.error="Fehler"
|
||||
AdvSceneSwitcher.mediaTab.states.playlistEnd="Ended(Playlist)"
|
||||
AdvSceneSwitcher.mediaTab.states.any="Beliebig"
|
||||
AdvSceneSwitcher.mediaTab.timeRestriction.none="Keine Auswahl"
|
||||
AdvSceneSwitcher.mediaTab.timeRestriction.shorter="Dauer kürzer"
|
||||
@@ -190,10 +725,10 @@ AdvSceneSwitcher.sceneSequenceTab.loadFail="Laden der Einstellungen ist fehlgesc
|
||||
AdvSceneSwitcher.sceneSequenceTab.loadSuccess="Laden der Einstellungen war erfolgreich!"
|
||||
AdvSceneSwitcher.sceneSequenceTab.fileType="Text Dateien (*.txt)"
|
||||
AdvSceneSwitcher.sceneSequenceTab.interruptible="kann unterbrochen werden"
|
||||
AdvSceneSwitcher.sceneSequenceTab.interruptibleHint="Andere Szenenwechselmethoden können diese Sequenz unterbrechen"
|
||||
AdvSceneSwitcher.sceneSequenceTab.interruptibleHint="Andere Szenenwechselmethoden können diese Sequenz unterbrechen"
|
||||
AdvSceneSwitcher.sceneSequenceTab.entry="Wenn {{startScenes}} aktiv ist wechsle zu {{scenes}} nach {{delay}} mit {{transitions}} {{interruptible}}"
|
||||
AdvSceneSwitcher.sceneSequenceTab.extendEdit="Szenenfolge erweitern"
|
||||
AdvSceneSwitcher.sceneSequenceTab.extendEntry="Nach {{delay}} wechsle zu {{scenes}} mit {{transitions}}"
|
||||
AdvSceneSwitcher.sceneSequenceTab.entry="Wenn {{startScenes}} aktiv ist wechsle zu {{scenes}} nach {{delay}} mit {{transitions}} {{interruptible}}"
|
||||
AdvSceneSwitcher.sceneSequenceTab.help="Dieser Tab ermöglicht es automatisiert zu einer anderen Szene zu wechseln, nachdem eine Szene für die konfigurierte Zeit aktiv war.\nZum Beispiel kann so automatisch zwischen zwei Szenen hin und her gewechselt werden.\n\nKlicke auf das markierte Plus Symbol, um einen neuen Eintrag hinzuzufügen."
|
||||
|
||||
; Audio Tab
|
||||
@@ -221,6 +756,7 @@ AdvSceneSwitcher.videoTab.help="<html><head/><body><p>Dieser Tab ermöglicht es
|
||||
|
||||
; Network Tab
|
||||
AdvSceneSwitcher.networkTab.title="Netzwerk"
|
||||
AdvSceneSwitcher.networkTab.description="Über diesen Tab kann die aktive Szene einer anderen OBS-Instanz ferngesteuert werden.\nBitte beachten, dass die Szenennamen auf allen OBS-Instanzen exakt übereinstimmen müssen."
|
||||
AdvSceneSwitcher.networkTab.warning="Die Verwendung des Servers außerhalb eines lokalen Netzwerks kann dazu führen, dass die aktive Szene von dritten Personen ausgelesen werden kann."
|
||||
AdvSceneSwitcher.networkTab.server="Server starten (Sendet Szenenwechselnachrichten zu allen verbundenen Clients)"
|
||||
AdvSceneSwitcher.networkTab.server.port="Port"
|
||||
@@ -287,35 +823,111 @@ AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopVirtualCamera="Virtuelle
|
||||
AdvSceneSwitcher.sceneTriggerTab.entry="Wenn {{scenes}} {{triggers}} {{actions}} {{audioSources}} nach {{duration}} "
|
||||
AdvSceneSwitcher.sceneTriggerTab.help="Dieser Tab ermöglicht es automatisiert Handlungen, wie etwa das Stoppen von einer Aufnahme oder des Streamens, bei einem Szenenwechsel auszuführen."
|
||||
|
||||
|
||||
; Hotkey
|
||||
AdvSceneSwitcher.hotkey.startSwitcherHotkey="Starte den Erweiteren Szenenwechsler"
|
||||
AdvSceneSwitcher.hotkey.stopSwitcherHotkey="Stoppe den Erweiteren Szenenwechsler"
|
||||
AdvSceneSwitcher.hotkey.startStopToggleSwitcherHotkey="Starte/Stoppe den Erweiteren Szenenwechsler"
|
||||
AdvSceneSwitcher.hotkey.startSwitcherHotkey="Starte den Erweiterten Szenenwechsler"
|
||||
AdvSceneSwitcher.hotkey.stopSwitcherHotkey="Stoppe den Erweiterten Szenenwechsler"
|
||||
AdvSceneSwitcher.hotkey.startStopToggleSwitcherHotkey="Starte/Stoppe den Erweiterten Szenenwechsler"
|
||||
AdvSceneSwitcher.hotkey.macro.pause="Pausiere Makro %1"
|
||||
AdvSceneSwitcher.hotkey.macro.unpause="Makro %1 nicht mehr pausieren"
|
||||
AdvSceneSwitcher.hotkey.macro.togglePause="Pause von Makro %1 togglen"
|
||||
AdvSceneSwitcher.hotkey.upMacroSegmentHotkey="Makro-Segmentauswahl nach oben verschieben"
|
||||
AdvSceneSwitcher.hotkey.downMacroSegmentHotkey="Makro-Segmentauswahl nach unten verschieben"
|
||||
AdvSceneSwitcher.hotkey.removeMacroSegmentHotkey="Ausgewähltes Makro-Segment entfernen"
|
||||
|
||||
AdvSceneSwitcher.askBackup="Neue Version des Erweiterten Automatischen Szenenwechslers wurde erkannt.\nSoll ein Backup der alten Einstellungen angelegt werden?"
|
||||
AdvSceneSwitcher.askForMacro="Makro auswählen {{macroSelection}}"
|
||||
|
||||
AdvSceneSwitcher.close="Schließen"
|
||||
AdvSceneSwitcher.browse="Öffnen"
|
||||
|
||||
AdvSceneSwitcher.item.select="--Element auswählen--"
|
||||
AdvSceneSwitcher.item.add="Neues Element hinzufügen"
|
||||
AdvSceneSwitcher.item.rename="Umbenennen"
|
||||
AdvSceneSwitcher.item.remove="Entfernen"
|
||||
AdvSceneSwitcher.item.properties="Einstellungen"
|
||||
AdvSceneSwitcher.item.newName="Neuer Name:"
|
||||
AdvSceneSwitcher.item.emptyName="Leerer Name nicht zulässig!"
|
||||
AdvSceneSwitcher.item.nameNotAvailable="Name nicht verfügbar!"
|
||||
AdvSceneSwitcher.item.nameReserved="Name reserviert!"
|
||||
|
||||
AdvSceneSwitcher.variable.select="--Variable auswählen--"
|
||||
AdvSceneSwitcher.variable.add="Neue Variable hinzufügen"
|
||||
AdvSceneSwitcher.variable.name="Name:"
|
||||
AdvSceneSwitcher.variable.value="Aktueller Wert:"
|
||||
AdvSceneSwitcher.variable.save="Verhalten speichern / laden"
|
||||
AdvSceneSwitcher.variable.save.dontSave="Variablenwert nicht speichern"
|
||||
AdvSceneSwitcher.variable.save.save="Variablenwert speichern"
|
||||
AdvSceneSwitcher.variable.save.default="Auf Wert setzen"
|
||||
|
||||
AdvSceneSwitcher.connection.select="--Verbindung auswählen--"
|
||||
AdvSceneSwitcher.connection.add="Neue Verbindung hinzufügen"
|
||||
AdvSceneSwitcher.connection.name="Name:"
|
||||
AdvSceneSwitcher.connection.address="Adresse:"
|
||||
AdvSceneSwitcher.connection.port="Port:"
|
||||
AdvSceneSwitcher.connection.password="Passwort:"
|
||||
AdvSceneSwitcher.connection.reconnect="Automatisch neu verbinden:"
|
||||
AdvSceneSwitcher.connection.reconnectDelay="Automatisch neu verbinden nach:"
|
||||
AdvSceneSwitcher.connection.connectOnStart="Beim Start verbinden:"
|
||||
AdvSceneSwitcher.connection.test="Verbindung testen"
|
||||
AdvSceneSwitcher.connection.status.disconnected="Verbindung getrennt"
|
||||
AdvSceneSwitcher.connection.status.connecting="Verbindung wird aufgebaut"
|
||||
AdvSceneSwitcher.connection.status.connected="Verbunden, aber nicht authentifiziert"
|
||||
AdvSceneSwitcher.connection.status.authenticated="Verbunden und authentifiziert"
|
||||
|
||||
AdvSceneSwitcher.regex.enable="Reguläre Ausdrücke aktivieren"
|
||||
AdvSceneSwitcher.regex.partialMatch="Teilweise Übereinstimmung zulassen"
|
||||
AdvSceneSwitcher.regex.caseInsensitive="Groß- und Kleinschreibung wird nicht berücksichtigt"
|
||||
AdvSceneSwitcher.regex.dotMatchNewline=". erkennt Zeilenumbrüche"
|
||||
AdvSceneSwitcher.regex.multiLine="^ und $ erkennen Start/Ende einer Zeile"
|
||||
AdvSceneSwitcher.regex.extendedPattern="Qt's ExtendedPatternSyntax aktivieren"
|
||||
|
||||
AdvSceneSwitcher.selectScene="--Szene auswählen--"
|
||||
AdvSceneSwitcher.selectPreviousScene="Vorhergehende Szene"
|
||||
AdvSceneSwitcher.currentTransition="Aktueller Szenenübergang"
|
||||
AdvSceneSwitcher.selectTransition="--Szenenübergang auswählen--"
|
||||
AdvSceneSwitcher.selectPreviousScene="Vorherige Szene"
|
||||
AdvSceneSwitcher.selectCurrentScene="Aktuelle Szene"
|
||||
AdvSceneSwitcher.selectPreviewScene="Vorschau-Szene"
|
||||
AdvSceneSwitcher.selectAnyScene="Beliebige Szene"
|
||||
AdvSceneSwitcher.currentTransition="Aktueller Übergang"
|
||||
AdvSceneSwitcher.anyTransition="Beliebiger Übergang"
|
||||
AdvSceneSwitcher.selectTransition="--Übergang auswählen--"
|
||||
AdvSceneSwitcher.selectWindow="--Fenster auswählen--"
|
||||
AdvSceneSwitcher.selectAudioSource="--Audio Quelle auswählen--"
|
||||
AdvSceneSwitcher.selectVideoSource="--Video Quelle auswählen--"
|
||||
AdvSceneSwitcher.selectMediaSource="--Medien Quelle auswählen--"
|
||||
AdvSceneSwitcher.selectSource="--Quelle auswählen--"
|
||||
AdvSceneSwitcher.selectAudioSource="--Audio-Quelle auswählen--"
|
||||
AdvSceneSwitcher.selectVideoSource="--Video-Quelle auswählen--"
|
||||
AdvSceneSwitcher.OBSVideoOutput="OBS's Haupt-Ausgabe"
|
||||
AdvSceneSwitcher.selectMediaSource="--Medien-Quelle auswählen--"
|
||||
AdvSceneSwitcher.selectProcess="--Prozess auswählen--"
|
||||
AdvSceneSwitcher.selectFilter="--Filter auswählen--"
|
||||
AdvSceneSwitcher.selectMacro="--Makro auswählen--"
|
||||
AdvSceneSwitcher.selectItem="--Element auswählen--"
|
||||
AdvSceneSwitcher.selectProfile="--Profil auswählen--"
|
||||
AdvSceneSwitcher.selectSceneCollection="--Szenensammlung auswählen--"
|
||||
AdvSceneSwitcher.enterPath="--Pfad eingeben--"
|
||||
AdvSceneSwitcher.enterText="--Text eingeben--"
|
||||
AdvSceneSwitcher.invaildEntriesWillNotBeSaved="Ungültige Einträge werden nicht gespeichert"
|
||||
AdvSceneSwitcher.selectWindowTip="Benutze \"OBS\" um das OBS-Studio Fenster auszuwählen\nBenutze\"Task Switching\"um die ALT + TAB Tastenkombination auszuwählen"
|
||||
AdvSceneSwitcher.enterURL="--URL eingebenL--"
|
||||
AdvSceneSwitcher.invaildEntriesWillNotBeSaved="ungültige Einträge werden nicht gespeichert"
|
||||
AdvSceneSwitcher.selectWindowTip="Verwende \"OBS\", um das OBS-Fenster zu definieren.\n Verwende \"Task Switching\", um ALT + TAB festzulegen."
|
||||
AdvSceneSwitcher.sceneItemSelection.all="Alle"
|
||||
AdvSceneSwitcher.sceneItemSelection.any="Beliebige"
|
||||
|
||||
AdvSceneSwitcher.status.active="Aktiv"
|
||||
AdvSceneSwitcher.status.inactive="Inaktiv"
|
||||
AdvSceneSwitcher.running="Plugin läuft"
|
||||
AdvSceneSwitcher.stopped="Plugin gestoppt"
|
||||
|
||||
AdvSceneSwitcher.firstBootMessage="<html><head/><body><p>Dies scheint das erste Mal zu sein, dass der Erweiterte Szenenwechsler gestartet wurde.<br>Bitte schaue ins <a href=\"https://github.com/WarmUpTill/SceneSwitcher/wiki\"><span style=\" text-decoration: underline; color:#268bd2;\">Wiki</span></a> für eine Liste von Anleitungen und Beispielen.<br>Nicht zögern und Fragen im <a href=\"https://obsproject.com/forum/threads/advanced-scene-switcher.48264\"><span style=\" text-decoration: underline; color:#268bd2;\">Thread</span></a> des Plugins im OBS-Forum stellen!!</p></body></html>"
|
||||
|
||||
AdvSceneSwitcher.deprecatedTabWarning="Die Entwicklung für dieses Tab wurde gestoppt!\nBitte stattdessen zur Verwendung des Makros-Tab übergehen.\nDieser Hinweis kann im Allgemein-Tab deaktiviert werden."
|
||||
|
||||
AdvSceneSwitcher.unit.milliseconds="Millisekunden"
|
||||
AdvSceneSwitcher.unit.secends="Sekunden"
|
||||
AdvSceneSwitcher.unit.minutes="Minuten"
|
||||
AdvSceneSwitcher.unit.hours="Stunden"
|
||||
AdvSceneSwitcher.duration.condition.none="Kein Dauer-Modifikator"
|
||||
AdvSceneSwitcher.duration.condition.more="Mindestens für"
|
||||
AdvSceneSwitcher.duration.condition.equal="Für genau"
|
||||
AdvSceneSwitcher.duration.condition.less="Höchstens für"
|
||||
AdvSceneSwitcher.duration.condition.within="In den letzten"
|
||||
|
||||
AdvSceneSwitcher.audio.monitor.none="Monitoring aus"
|
||||
AdvSceneSwitcher.audio.monitor.monitorOnly="Nur Monitoring (Ausgabe stummschalten)"
|
||||
AdvSceneSwitcher.audio.monitor.both="Monitoring und Ausgabe"
|
||||
|
||||
@@ -17,7 +17,7 @@ AdvSceneSwitcher.generalTab.status.autoStart.never="Never"
|
||||
AdvSceneSwitcher.generalTab.status.autoStart.recording="Recording"
|
||||
AdvSceneSwitcher.generalTab.status.autoStart.streaming="Streaming"
|
||||
AdvSceneSwitcher.generalTab.status.autoStart.recordingAndStreaming="Recording or Streaming"
|
||||
AdvSceneSwitcher.generalTab.status.checkInterval="Check switch conditions every"
|
||||
AdvSceneSwitcher.generalTab.status.checkInterval="Check conditions every"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior="General behavior"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.onNoMet="If no actions are performed for"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.onNoMetDelayTooltip="Will only ever be as accurate as the configured check interval."
|
||||
@@ -82,9 +82,13 @@ AdvSceneSwitcher.macroTab.defaultname="Macro %1"
|
||||
AdvSceneSwitcher.macroTab.defaultGroupName="Group %1"
|
||||
AdvSceneSwitcher.macroTab.exists="Macro name exists already"
|
||||
AdvSceneSwitcher.macroTab.groupDeleteConfirm="Are you sure you want to delete \"%1\" and all its elements?"
|
||||
AdvSceneSwitcher.macroTab.copy="Create copy of current macro"
|
||||
AdvSceneSwitcher.macroTab.group="Group selected items"
|
||||
AdvSceneSwitcher.macroTab.ungroup="Ungroup selected groups"
|
||||
AdvSceneSwitcher.macroTab.deleteMultipleMacrosConfirmation="Are you sure you want to delete %1 macros?"
|
||||
AdvSceneSwitcher.macroTab.contextMenuAdd="Add"
|
||||
AdvSceneSwitcher.macroTab.copy="Duplicate Macro"
|
||||
AdvSceneSwitcher.macroTab.group="Group Selected Macros"
|
||||
AdvSceneSwitcher.macroTab.ungroup="Ungroup Selected Groups"
|
||||
AdvSceneSwitcher.macroTab.rename="Rename"
|
||||
AdvSceneSwitcher.macroTab.remove="Remove"
|
||||
AdvSceneSwitcher.macroTab.expandAll="Expand all"
|
||||
AdvSceneSwitcher.macroTab.collapseAll="Collapse all"
|
||||
AdvSceneSwitcher.macroTab.maximize="Maximize"
|
||||
@@ -174,6 +178,7 @@ AdvSceneSwitcher.condition.video.condition.noImage="has no output"
|
||||
AdvSceneSwitcher.condition.video.condition.pattern="matches pattern"
|
||||
AdvSceneSwitcher.condition.video.condition.object="contains object"
|
||||
AdvSceneSwitcher.condition.video.condition.brightness="brightness"
|
||||
AdvSceneSwitcher.condition.video.condition.ocr="contains text"
|
||||
AdvSceneSwitcher.condition.video.askFileAction="Do you want to use an existing file or create a screenshot of the currently selected source?"
|
||||
AdvSceneSwitcher.condition.video.askFileAction.file="Use existing file"
|
||||
AdvSceneSwitcher.condition.video.askFileAction.screenshot="Create screenshot"
|
||||
@@ -184,6 +189,11 @@ AdvSceneSwitcher.condition.video.usePatternForChangedCheck.tooltip="This will al
|
||||
AdvSceneSwitcher.condition.video.patternThreshold="Threshold: "
|
||||
AdvSceneSwitcher.condition.video.patternThresholdDescription="A higher threshold value means that the pattern needs to match the video source more closely."
|
||||
AdvSceneSwitcher.condition.video.patternThresholdUseAlphaAsMask="Use alpha channel as mask for pattern."
|
||||
AdvSceneSwitcher.condition.video.patternMatchMode="Use pattern matching mode {{patternMatchingModes}}"
|
||||
AdvSceneSwitcher.condition.video.patternMatchMode.crossCorrelation="Cross correlation"
|
||||
AdvSceneSwitcher.condition.video.patternMatchMode.correlationCoefficient="Correlation coefficient"
|
||||
AdvSceneSwitcher.condition.video.patternMatchMode.squaredDifference="Squared difference"
|
||||
AdvSceneSwitcher.condition.video.patternMatchMode.tip="The best method to use depends on the specific problem and the characteristics of the images involved.\nIn practice, it's often necessary to experiment with different methods and parameters to find the best match for a particular use case."
|
||||
AdvSceneSwitcher.condition.video.brightnessThreshold="Average brightness is above:"
|
||||
AdvSceneSwitcher.condition.video.brightnessThresholdDescription="A high value is indicating a bright image and a low one a darker one."
|
||||
AdvSceneSwitcher.condition.video.currentBrightness="Current average brightness: %1"
|
||||
@@ -198,7 +208,18 @@ AdvSceneSwitcher.condition.video.patternMatchFail="Pattern was not found!"
|
||||
AdvSceneSwitcher.condition.video.patternMatchSuccess="Pattern is highlighted in red"
|
||||
AdvSceneSwitcher.condition.video.objectMatchFail="Object was not found!"
|
||||
AdvSceneSwitcher.condition.video.objectMatchSuccess="Object is highlighted in red"
|
||||
AdvSceneSwitcher.condition.video.ocrMatchSuccess="Detected text:\n\n%1"
|
||||
AdvSceneSwitcher.condition.video.modelLoadFail="Model data could not be loaded!"
|
||||
AdvSceneSwitcher.condition.video.selectColor="Select Color"
|
||||
AdvSceneSwitcher.condition.video.ocrMode.singleColumn="Single column of text of variable sizes"
|
||||
AdvSceneSwitcher.condition.video.ocrMode.singleBlockVertText="Single uniform block of vertically aligned text"
|
||||
AdvSceneSwitcher.condition.video.ocrMode.singleBlock="Single uniform block of text"
|
||||
AdvSceneSwitcher.condition.video.ocrMode.singleLine="Single text line"
|
||||
AdvSceneSwitcher.condition.video.ocrMode.singleWord="Single word"
|
||||
AdvSceneSwitcher.condition.video.ocrMode.circleWord="Single word in a circle"
|
||||
AdvSceneSwitcher.condition.video.ocrMode.singleChar="Single character"
|
||||
AdvSceneSwitcher.condition.video.ocrMode.sparseText="Find text in no particular order"
|
||||
AdvSceneSwitcher.condition.video.ocrMode.sparseTextOSD="Find text in no particular order (OSD)"
|
||||
AdvSceneSwitcher.condition.video.type.main="OBS's main output"
|
||||
AdvSceneSwitcher.condition.video.type.source="Source"
|
||||
AdvSceneSwitcher.condition.video.type.scene="Scene"
|
||||
@@ -208,6 +229,8 @@ AdvSceneSwitcher.condition.video.entry.minNeighbor="Minimum neighbors: {{minNeig
|
||||
AdvSceneSwitcher.condition.video.entry.throttle="{{throttleEnable}}Reduce CPU load by performing check only every {{throttleCount}} milliseconds"
|
||||
AdvSceneSwitcher.condition.video.entry.checkAreaEnable="Perform check only in area"
|
||||
AdvSceneSwitcher.condition.video.entry.checkArea="{{checkAreaEnable}}{{checkArea}}{{selectArea}}"
|
||||
AdvSceneSwitcher.condition.video.entry.orcColorPick="Check for text color:{{textColor}}{{selectColor}}"
|
||||
AdvSceneSwitcher.condition.video.entry.orcTextType="Check for text type:{{textType}}"
|
||||
AdvSceneSwitcher.condition.video.minSize="Minimum size:"
|
||||
AdvSceneSwitcher.condition.video.maxSize="Maximum size:"
|
||||
AdvSceneSwitcher.condition.video.selectArea="Select area"
|
||||
@@ -583,11 +606,28 @@ AdvSceneSwitcher.action.variable.type.increment="Increment"
|
||||
AdvSceneSwitcher.action.variable.type.decrement="Decrement"
|
||||
AdvSceneSwitcher.action.variable.type.setActionValue="Set to action value"
|
||||
AdvSceneSwitcher.action.variable.type.setConditionValue="Set to condition value"
|
||||
AdvSceneSwitcher.action.variable.type.roundToInt="Round to nearest integer"
|
||||
AdvSceneSwitcher.action.variable.type.subString="Set to substring of current value"
|
||||
AdvSceneSwitcher.action.variable.subString.begin="Beginning"
|
||||
AdvSceneSwitcher.action.variable.subString.all="All"
|
||||
AdvSceneSwitcher.action.variable.invalidSelection="Invalid selection!"
|
||||
AdvSceneSwitcher.action.variable.actionNoVariableSupport="Getting variable values from %1 actions is not supported!"
|
||||
AdvSceneSwitcher.action.variable.conditionNoVariableSupport="Getting variable values from %1 conditions is not supported!"
|
||||
AdvSceneSwitcher.action.variable.currentSegmentValue="Current value:"
|
||||
AdvSceneSwitcher.action.variable.entry="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}"
|
||||
AdvSceneSwitcher.action.variable.entry.substringIndex="Substring start:{{subStringStart}} Substring size:{{subStringSize}}"
|
||||
AdvSceneSwitcher.action.variable.entry.substringRegex="Assign value of{{regexMatchIdx}}match using regular expression:"
|
||||
AdvSceneSwitcher.action.projector="Projector"
|
||||
AdvSceneSwitcher.action.projector.type.source="Source"
|
||||
AdvSceneSwitcher.action.projector.type.scene="Scene"
|
||||
AdvSceneSwitcher.action.projector.type.preview="Preview"
|
||||
AdvSceneSwitcher.action.projector.type.program="Program"
|
||||
AdvSceneSwitcher.action.projector.type.multiview="Multiview"
|
||||
AdvSceneSwitcher.action.projector.display="Display"
|
||||
AdvSceneSwitcher.action.projector.windowed="Windowed"
|
||||
AdvSceneSwitcher.action.projector.fullscreen="Fullscreen"
|
||||
AdvSceneSwitcher.action.projector.entry="Open{{windowTypes}}projector of{{types}}{{scenes}}{{sources}}"
|
||||
AdvSceneSwitcher.action.projector.entry.monitor="on{{monitors}}"
|
||||
|
||||
|
||||
; Transition Tab
|
||||
|
||||
BIN
data/res/ocr/eng.traineddata
Normal file
BIN
data/res/ocr/eng.traineddata
Normal file
Binary file not shown.
1
deps/leptonica
vendored
Submodule
1
deps/leptonica
vendored
Submodule
Submodule deps/leptonica added at b667978e86
1
deps/tesseract
vendored
Submodule
1
deps/tesseract
vendored
Submodule
Submodule deps/tesseract added at 080da83cc5
@@ -155,6 +155,7 @@ void SwitcherData::Thread()
|
||||
|
||||
while (true) {
|
||||
std::unique_lock<std::mutex> lock(m);
|
||||
mainLoopLock = &lock;
|
||||
|
||||
bool match = false;
|
||||
OBSWeakSource scene;
|
||||
@@ -230,19 +231,6 @@ void SwitcherData::Thread()
|
||||
|
||||
ClearWebsocketMessages();
|
||||
|
||||
// After this point we will call frontend functions like
|
||||
// obs_frontend_set_current_scene() and
|
||||
// obs_frontend_set_current_transition()
|
||||
//
|
||||
// During this time SaveSceneSwitcher() could be called
|
||||
// leading to a deadlock with the frontend function being stuck
|
||||
// in QMetaObject::invokeMethod() holding the mutex and
|
||||
// OBS being stuck in SaveSceneSwitcher().
|
||||
//
|
||||
// So we have to unlock() risking race conditions as these are
|
||||
// less frequent than the above described deadlock
|
||||
lock.unlock();
|
||||
|
||||
if (match) {
|
||||
if (macroMatch) {
|
||||
runMacros();
|
||||
@@ -256,6 +244,7 @@ void SwitcherData::Thread()
|
||||
switcher->firstIntervalAfterStop = false;
|
||||
}
|
||||
|
||||
mainLoopLock = nullptr;
|
||||
blog(LOG_INFO, "stopped");
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,10 @@ public:
|
||||
|
||||
bool addNewMacro(std::shared_ptr<Macro> &res, std::string &name,
|
||||
std::string format = "");
|
||||
void RemoveMacro(std::shared_ptr<Macro> &);
|
||||
void RenameMacro(std::shared_ptr<Macro> &, const QString &name);
|
||||
std::shared_ptr<Macro> getSelectedMacro();
|
||||
std::vector<std::shared_ptr<Macro>> getSelectedMacros();
|
||||
void SetEditMacro(Macro &m);
|
||||
void SetMacroEditAreaDisabled(bool);
|
||||
void HighlightAction(int idx, QColor color = QColor(Qt::green));
|
||||
@@ -145,6 +148,7 @@ public slots:
|
||||
void ShowMacroActionsContextMenu(const QPoint &);
|
||||
void ShowMacroConditionsContextMenu(const QPoint &);
|
||||
void CopyMacro();
|
||||
void RenameCurrentMacro();
|
||||
void ExpandAllActions();
|
||||
void ExpandAllConditions();
|
||||
void CollapseAllActions();
|
||||
|
||||
@@ -255,11 +255,28 @@ QString getDefaultSaveLocation()
|
||||
{
|
||||
QString desktopPath = QStandardPaths::writableLocation(
|
||||
QStandardPaths::DesktopLocation);
|
||||
|
||||
auto scName = obs_frontend_get_current_scene_collection();
|
||||
QString sceneCollectionName(scName);
|
||||
bfree(scName);
|
||||
|
||||
auto timestamp = QDateTime::currentDateTime();
|
||||
return desktopPath + "/adv-ss-" + sceneCollectionName + "-" +
|
||||
auto path = desktopPath + "/adv-ss-" + sceneCollectionName + "-" +
|
||||
timestamp.toString("yyyy.MM.dd.hh.mm.ss");
|
||||
|
||||
// Check if scene collection name contains invalid path characters
|
||||
QFile file(path);
|
||||
if (file.exists()) {
|
||||
return path;
|
||||
}
|
||||
|
||||
bool validPath = file.open(QIODevice::WriteOnly);
|
||||
if (validPath) {
|
||||
file.remove();
|
||||
return path;
|
||||
}
|
||||
|
||||
return desktopPath + "/adv-ss-" +
|
||||
timestamp.toString("yyyy.MM.dd.hh.mm.ss");
|
||||
}
|
||||
|
||||
@@ -636,12 +653,11 @@ void SwitcherData::saveGeneralSettings(obs_data_t *obj)
|
||||
|
||||
obs_data_set_int(obj, "threadPriority", threadPriority);
|
||||
|
||||
obs_data_set_bool(obj, "tansitionOverrideOverride",
|
||||
obs_data_set_bool(obj, "transitionOverrideOverride",
|
||||
transitionOverrideOverride);
|
||||
obs_data_set_default_bool(obj, "adjustActiveTransitionType",
|
||||
adjustActiveTransitionType);
|
||||
obs_data_set_bool(obj, "adjustActiveTransitionType",
|
||||
adjustActiveTransitionType);
|
||||
|
||||
obs_data_set_string(obj, "lastImportPath", lastImportPath.c_str());
|
||||
}
|
||||
|
||||
@@ -719,11 +735,23 @@ void SwitcherData::loadGeneralSettings(obs_data_t *obj)
|
||||
QThread::NormalPriority);
|
||||
threadPriority = obs_data_get_int(obj, "threadPriority");
|
||||
|
||||
transitionOverrideOverride =
|
||||
obs_data_get_bool(obj, "tansitionOverrideOverride");
|
||||
// TODO: Remove this fallback in future version
|
||||
if (obs_data_has_user_value(obj, "tansitionOverrideOverride")) {
|
||||
transitionOverrideOverride =
|
||||
obs_data_get_bool(obj, "tansitionOverrideOverride");
|
||||
} else {
|
||||
transitionOverrideOverride =
|
||||
obs_data_get_bool(obj, "transitionOverrideOverride");
|
||||
}
|
||||
adjustActiveTransitionType =
|
||||
obs_data_get_bool(obj, "adjustActiveTransitionType");
|
||||
|
||||
if (!transitionOverrideOverride && !adjustActiveTransitionType) {
|
||||
blog(LOG_INFO,
|
||||
"reset transition behaviour to adjust active transition type");
|
||||
adjustActiveTransitionType = true;
|
||||
}
|
||||
|
||||
lastImportPath = obs_data_get_string(obj, "lastImportPath");
|
||||
}
|
||||
|
||||
|
||||
290
src/macro-core/macro-action-projector.cpp
Normal file
290
src/macro-core/macro-action-projector.cpp
Normal file
@@ -0,0 +1,290 @@
|
||||
#include "macro-action-projector.hpp"
|
||||
#include "advanced-scene-switcher.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include <QScreen>
|
||||
|
||||
const std::string MacroActionProjector::id = "projector";
|
||||
|
||||
bool MacroActionProjector::_registered = MacroActionFactory::Register(
|
||||
MacroActionProjector::id,
|
||||
{MacroActionProjector::Create, MacroActionProjectorEdit::Create,
|
||||
"AdvSceneSwitcher.action.projector"});
|
||||
|
||||
const static std::map<MacroActionProjector::Type, std::string> selectionTypes = {
|
||||
{MacroActionProjector::Type::SOURCE,
|
||||
"AdvSceneSwitcher.action.projector.type.source"},
|
||||
{MacroActionProjector::Type::SCENE,
|
||||
"AdvSceneSwitcher.action.projector.type.scene"},
|
||||
{MacroActionProjector::Type::PREVIEW,
|
||||
"AdvSceneSwitcher.action.projector.type.preview"},
|
||||
{MacroActionProjector::Type::PROGRAM,
|
||||
"AdvSceneSwitcher.action.projector.type.program"},
|
||||
{MacroActionProjector::Type::MULTIVIEW,
|
||||
"AdvSceneSwitcher.action.projector.type.multiview"},
|
||||
};
|
||||
|
||||
bool MacroActionProjector::PerformAction()
|
||||
{
|
||||
std::string name = "";
|
||||
const char *type = "";
|
||||
|
||||
switch (_type) {
|
||||
case MacroActionProjector::Type::SOURCE:
|
||||
type = "Source";
|
||||
name = GetWeakSourceName(_source.GetSource());
|
||||
if (name.empty()) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case MacroActionProjector::Type::SCENE:
|
||||
type = "Scene";
|
||||
name = GetWeakSourceName(_scene.GetScene());
|
||||
if (name.empty()) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case MacroActionProjector::Type::PREVIEW:
|
||||
type = "Preview";
|
||||
break;
|
||||
case MacroActionProjector::Type::PROGRAM:
|
||||
type = "StudioProgram";
|
||||
break;
|
||||
case MacroActionProjector::Type::MULTIVIEW:
|
||||
type = "Multiview";
|
||||
break;
|
||||
}
|
||||
|
||||
obs_frontend_open_projector(type, _fullscreen ? _monitor : -1, "",
|
||||
name.c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MacroActionProjector::LogAction() const
|
||||
{
|
||||
auto it = selectionTypes.find(_type);
|
||||
if (it != selectionTypes.end()) {
|
||||
vblog(LOG_INFO,
|
||||
"performed projector action \"%s\" with"
|
||||
"source \"%s\","
|
||||
"scene \"%s\","
|
||||
"monitor %d",
|
||||
it->second.c_str(), _source.ToString(true).c_str(),
|
||||
_scene.ToString(true).c_str(), _monitor);
|
||||
} else {
|
||||
blog(LOG_WARNING, "ignored unknown projector action %d",
|
||||
static_cast<int>(_type));
|
||||
}
|
||||
}
|
||||
|
||||
bool MacroActionProjector::Save(obs_data_t *obj) const
|
||||
{
|
||||
MacroAction::Save(obj);
|
||||
obs_data_set_int(obj, "type", static_cast<int>(_type));
|
||||
obs_data_set_int(obj, "monitor", _monitor);
|
||||
obs_data_set_bool(obj, "fullscreen", _fullscreen);
|
||||
_scene.Save(obj);
|
||||
_source.Save(obj);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MacroActionProjector::Load(obs_data_t *obj)
|
||||
{
|
||||
MacroAction::Load(obj);
|
||||
_type = static_cast<Type>(obs_data_get_int(obj, "type"));
|
||||
_monitor = obs_data_get_int(obj, "monitor");
|
||||
_fullscreen = obs_data_get_bool(obj, "fullscreen");
|
||||
_scene.Load(obj);
|
||||
_source.Load(obj);
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline void populateSelectionTypes(QComboBox *list)
|
||||
{
|
||||
for (auto entry : selectionTypes) {
|
||||
list->addItem(obs_module_text(entry.second.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
static inline void populateWindowTypes(QComboBox *list)
|
||||
{
|
||||
list->addItem(
|
||||
obs_module_text("AdvSceneSwitcher.action.projector.windowed"));
|
||||
list->addItem(obs_module_text(
|
||||
"AdvSceneSwitcher.action.projector.fullscreen"));
|
||||
}
|
||||
|
||||
static QStringList getMonitorNames()
|
||||
{
|
||||
QStringList monitorNames;
|
||||
QList<QScreen *> screens = QGuiApplication::screens();
|
||||
for (int i = 0; i < screens.size(); i++) {
|
||||
QScreen *screen = screens[i];
|
||||
QRect screenGeometry = screen->geometry();
|
||||
qreal ratio = screen->devicePixelRatio();
|
||||
QString name = "";
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
name = screen->name();
|
||||
#else
|
||||
name = screen->model().simplified();
|
||||
if (name.length() > 1 && name.endsWith("-")) {
|
||||
name.chop(1);
|
||||
}
|
||||
#endif
|
||||
name = name.simplified();
|
||||
|
||||
if (name.length() == 0) {
|
||||
name = QString("%1 %2")
|
||||
.arg(obs_module_text(
|
||||
"AdvSceneSwitcher.action.projector.display"))
|
||||
.arg(QString::number(i + 1));
|
||||
}
|
||||
QString str =
|
||||
QString("%1: %2x%3 @ %4,%5")
|
||||
.arg(name,
|
||||
QString::number(screenGeometry.width() *
|
||||
ratio),
|
||||
QString::number(screenGeometry.height() *
|
||||
ratio),
|
||||
QString::number(screenGeometry.x()),
|
||||
QString::number(screenGeometry.y()));
|
||||
|
||||
monitorNames << str;
|
||||
}
|
||||
return monitorNames;
|
||||
}
|
||||
|
||||
MacroActionProjectorEdit::MacroActionProjectorEdit(
|
||||
QWidget *parent, std::shared_ptr<MacroActionProjector> entryData)
|
||||
: QWidget(parent),
|
||||
_windowTypes(new QComboBox()),
|
||||
_types(new QComboBox()),
|
||||
_scenes(new SceneSelectionWidget(window(), true, false, true, true,
|
||||
true)),
|
||||
_sources(new SourceSelectionWidget(window(), QStringList(), true)),
|
||||
_monitorSelection(new QHBoxLayout()),
|
||||
_monitors(new QComboBox())
|
||||
{
|
||||
populateWindowTypes(_windowTypes);
|
||||
populateSelectionTypes(_types);
|
||||
auto sources = GetSourceNames();
|
||||
sources.sort();
|
||||
_sources->SetSourceNameList(sources);
|
||||
_monitors->addItems(getMonitorNames());
|
||||
|
||||
QWidget::connect(_windowTypes, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(WindowTypeChanged(int)));
|
||||
QWidget::connect(_types, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(TypeChanged(int)));
|
||||
QWidget::connect(_scenes, SIGNAL(SceneChanged(const SceneSelection &)),
|
||||
this, SLOT(SceneChanged(const SceneSelection &)));
|
||||
QWidget::connect(_sources,
|
||||
SIGNAL(SourceChanged(const SourceSelection &)), this,
|
||||
SLOT(SourceChanged(const SourceSelection &)));
|
||||
QWidget::connect(_monitors, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(MonitorChanged(int)));
|
||||
|
||||
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{windowTypes}}", _windowTypes}, {"{{types}}", _types},
|
||||
{"{{scenes}}", _scenes}, {"{{sources}}", _sources},
|
||||
{"{{monitors}}", _monitors},
|
||||
};
|
||||
|
||||
placeWidgets(obs_module_text(
|
||||
"AdvSceneSwitcher.action.projector.entry.monitor"),
|
||||
_monitorSelection, widgetPlaceholders);
|
||||
|
||||
QHBoxLayout *mainLayout = new QHBoxLayout;
|
||||
placeWidgets(obs_module_text("AdvSceneSwitcher.action.projector.entry"),
|
||||
mainLayout, widgetPlaceholders);
|
||||
mainLayout->insertLayout(mainLayout->count() - 1, _monitorSelection);
|
||||
setLayout(mainLayout);
|
||||
|
||||
_entryData = entryData;
|
||||
UpdateEntryData();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void MacroActionProjectorEdit::UpdateEntryData()
|
||||
{
|
||||
if (!_entryData) {
|
||||
return;
|
||||
}
|
||||
_windowTypes->setCurrentIndex(_entryData->_fullscreen ? 1 : 0);
|
||||
_types->setCurrentIndex(static_cast<int>(_entryData->_type));
|
||||
_scenes->SetScene(_entryData->_scene);
|
||||
_sources->SetSource(_entryData->_source);
|
||||
_monitors->setCurrentIndex(_entryData->_monitor);
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroActionProjectorEdit::SceneChanged(const SceneSelection &s)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
_entryData->_scene = s;
|
||||
}
|
||||
|
||||
void MacroActionProjectorEdit::SourceChanged(const SourceSelection &source)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
_entryData->_source = source;
|
||||
}
|
||||
|
||||
void MacroActionProjectorEdit::MonitorChanged(int value)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
_entryData->_monitor = value;
|
||||
}
|
||||
|
||||
void MacroActionProjectorEdit::WindowTypeChanged(int value)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
_entryData->_fullscreen =
|
||||
_windowTypes->currentText() ==
|
||||
obs_module_text("AdvSceneSwitcher.action.projector.fullscreen");
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroActionProjectorEdit::TypeChanged(int value)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
_entryData->_type = static_cast<MacroActionProjector::Type>(value);
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroActionProjectorEdit::SetWidgetVisibility()
|
||||
{
|
||||
if (!_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
_scenes->setVisible(_entryData->_type ==
|
||||
MacroActionProjector::Type::SCENE);
|
||||
_sources->setVisible(_entryData->_type ==
|
||||
MacroActionProjector::Type::SOURCE);
|
||||
setLayoutVisible(_monitorSelection, _entryData->_fullscreen);
|
||||
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
73
src/macro-core/macro-action-projector.hpp
Normal file
73
src/macro-core/macro-action-projector.hpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "scene-selection.hpp"
|
||||
#include "source-selection.hpp"
|
||||
|
||||
class MacroActionProjector : public MacroAction {
|
||||
public:
|
||||
MacroActionProjector(Macro *m) : MacroAction(m) {}
|
||||
bool PerformAction();
|
||||
void LogAction() const;
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
std::string GetId() const { return id; };
|
||||
static std::shared_ptr<MacroAction> Create(Macro *m)
|
||||
{
|
||||
return std::make_shared<MacroActionProjector>(m);
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
SOURCE,
|
||||
SCENE,
|
||||
PREVIEW,
|
||||
PROGRAM,
|
||||
MULTIVIEW,
|
||||
};
|
||||
|
||||
Type _type = Type::SCENE;
|
||||
SourceSelection _source;
|
||||
SceneSelection _scene;
|
||||
int _monitor = 0;
|
||||
bool _fullscreen = true;
|
||||
|
||||
private:
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
};
|
||||
|
||||
class MacroActionProjectorEdit : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MacroActionProjectorEdit(
|
||||
QWidget *parent,
|
||||
std::shared_ptr<MacroActionProjector> entryData = nullptr);
|
||||
void UpdateEntryData();
|
||||
static QWidget *Create(QWidget *parent,
|
||||
std::shared_ptr<MacroAction> action)
|
||||
{
|
||||
return new MacroActionProjectorEdit(
|
||||
parent, std::dynamic_pointer_cast<MacroActionProjector>(
|
||||
action));
|
||||
}
|
||||
|
||||
private slots:
|
||||
void WindowTypeChanged(int value);
|
||||
void TypeChanged(int value);
|
||||
void SceneChanged(const SceneSelection &);
|
||||
void SourceChanged(const SourceSelection &);
|
||||
void MonitorChanged(int value);
|
||||
|
||||
protected:
|
||||
QComboBox *_windowTypes;
|
||||
QComboBox *_types;
|
||||
SceneSelectionWidget *_scenes;
|
||||
SourceSelectionWidget *_sources;
|
||||
QHBoxLayout *_monitorSelection;
|
||||
QComboBox *_monitors;
|
||||
std::shared_ptr<MacroActionProjector> _entryData;
|
||||
|
||||
private:
|
||||
void SetWidgetVisibility();
|
||||
bool _loading = true;
|
||||
};
|
||||
@@ -11,37 +11,39 @@ bool MacroActionSwitchScene::_registered = MacroActionFactory::Register(
|
||||
{MacroActionSwitchScene::Create, MacroActionSwitchSceneEdit::Create,
|
||||
"AdvSceneSwitcher.action.switchScene"});
|
||||
|
||||
void waitForTransitionChange(OBSWeakSource &transition)
|
||||
static void waitForTransitionChange(OBSWeakSource &transition,
|
||||
std::unique_lock<std::mutex> *lock,
|
||||
Macro *macro)
|
||||
{
|
||||
const auto time = 100ms;
|
||||
obs_source_t *source = obs_weak_source_get_source(transition);
|
||||
std::unique_lock<std::mutex> lock(switcher->m);
|
||||
|
||||
bool stillTransitioning = true;
|
||||
while (stillTransitioning && !switcher->abortMacroWait) {
|
||||
switcher->macroTransitionCv.wait_for(lock, time);
|
||||
while (stillTransitioning && !switcher->abortMacroWait &&
|
||||
!macro->GetStop()) {
|
||||
switcher->macroTransitionCv.wait_for(*lock, time);
|
||||
float t = obs_transition_get_time(source);
|
||||
stillTransitioning = t < 1.0f && t > 0.0f;
|
||||
}
|
||||
obs_source_release(source);
|
||||
}
|
||||
|
||||
void waitForTransitionChangeFixedDuration(int duration)
|
||||
static void waitForTransitionChangeFixedDuration(
|
||||
int duration, std::unique_lock<std::mutex> *lock, Macro *macro)
|
||||
{
|
||||
duration += 200; // It seems to be necessary to add a small buffer
|
||||
auto time = std::chrono::high_resolution_clock::now() +
|
||||
std::chrono::milliseconds(duration);
|
||||
|
||||
switcher->abortMacroWait = false;
|
||||
std::unique_lock<std::mutex> lock(switcher->m);
|
||||
while (!switcher->abortMacroWait) {
|
||||
if (switcher->macroTransitionCv.wait_until(lock, time) ==
|
||||
while (!switcher->abortMacroWait && !macro->GetStop()) {
|
||||
if (switcher->macroTransitionCv.wait_until(*lock, time) ==
|
||||
std::cv_status::timeout) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int getTransitionOverrideDuration(OBSWeakSource &scene)
|
||||
static int getTransitionOverrideDuration(OBSWeakSource &scene)
|
||||
{
|
||||
int duration = 0;
|
||||
obs_source_t *source = obs_weak_source_get_source(scene);
|
||||
@@ -55,7 +57,7 @@ int getTransitionOverrideDuration(OBSWeakSource &scene)
|
||||
return duration;
|
||||
}
|
||||
|
||||
bool isUsingFixedLengthTransition(const OBSWeakSource &transition)
|
||||
static bool isUsingFixedLengthTransition(const OBSWeakSource &transition)
|
||||
{
|
||||
obs_source_t *source = obs_weak_source_get_source(transition);
|
||||
bool ret = obs_transition_fixed(source);
|
||||
@@ -63,7 +65,7 @@ bool isUsingFixedLengthTransition(const OBSWeakSource &transition)
|
||||
return ret;
|
||||
}
|
||||
|
||||
OBSWeakSource getOverrideTransition(OBSWeakSource &scene)
|
||||
static OBSWeakSource getOverrideTransition(OBSWeakSource &scene)
|
||||
{
|
||||
OBSWeakSource transition;
|
||||
obs_source_t *source = obs_weak_source_get_source(scene);
|
||||
@@ -75,8 +77,8 @@ OBSWeakSource getOverrideTransition(OBSWeakSource &scene)
|
||||
return transition;
|
||||
}
|
||||
|
||||
int getExpectedTransitionDuration(OBSWeakSource &scene, OBSWeakSource &t,
|
||||
double duration)
|
||||
static int getExpectedTransitionDuration(OBSWeakSource &scene, OBSWeakSource &t,
|
||||
double duration)
|
||||
{
|
||||
OBSWeakSource transition = t;
|
||||
if (!switcher->transitionOverrideOverride) {
|
||||
@@ -97,6 +99,24 @@ int getExpectedTransitionDuration(OBSWeakSource &scene, OBSWeakSource &t,
|
||||
return obs_frontend_get_transition_duration();
|
||||
}
|
||||
|
||||
bool MacroActionSwitchScene::WaitForTransition(OBSWeakSource &scene,
|
||||
OBSWeakSource &transition)
|
||||
{
|
||||
const int expectedTransitionDuration = getExpectedTransitionDuration(
|
||||
scene, transition, _duration.seconds);
|
||||
switcher->abortMacroWait = false;
|
||||
|
||||
std::unique_lock<std::mutex> lock(switcher->m);
|
||||
if (expectedTransitionDuration < 0) {
|
||||
waitForTransitionChange(transition, &lock, GetMacro());
|
||||
} else {
|
||||
waitForTransitionChangeFixedDuration(expectedTransitionDuration,
|
||||
&lock, GetMacro());
|
||||
}
|
||||
|
||||
return !switcher->abortMacroWait;
|
||||
}
|
||||
|
||||
bool MacroActionSwitchScene::PerformAction()
|
||||
{
|
||||
auto scene = _scene.GetScene();
|
||||
@@ -104,16 +124,7 @@ bool MacroActionSwitchScene::PerformAction()
|
||||
switchScene({scene, transition, (int)(_duration.seconds * 1000)},
|
||||
obs_frontend_preview_program_mode_active());
|
||||
if (_blockUntilTransitionDone && scene) {
|
||||
const int expectedTransitionDuration =
|
||||
getExpectedTransitionDuration(scene, transition,
|
||||
_duration.seconds);
|
||||
if (expectedTransitionDuration < 0) {
|
||||
waitForTransitionChange(transition);
|
||||
} else {
|
||||
waitForTransitionChangeFixedDuration(
|
||||
expectedTransitionDuration);
|
||||
}
|
||||
return !switcher->abortMacroWait;
|
||||
return WaitForTransition(scene, transition);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public:
|
||||
bool _blockUntilTransitionDone = true;
|
||||
|
||||
private:
|
||||
const char *getType() { return "MacroActionSwitchScene"; }
|
||||
bool WaitForTransition(OBSWeakSource &scene, OBSWeakSource &transition);
|
||||
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
|
||||
@@ -45,6 +45,8 @@ bool MacroActionScreenshot::PerformAction()
|
||||
case MacroActionScreenshot::TargetType::SCENE:
|
||||
source = _scene.GetScene(false);
|
||||
break;
|
||||
case MacroActionScreenshot::TargetType::MAIN_OUTPUT:
|
||||
break;
|
||||
}
|
||||
|
||||
switch (_saveType) {
|
||||
@@ -73,8 +75,7 @@ void MacroActionScreenshot::LogAction() const
|
||||
_scene.ToString(true).c_str());
|
||||
break;
|
||||
case MacroActionScreenshot::TargetType::MAIN_OUTPUT:
|
||||
vblog(LOG_INFO, "trigger screenshot of main output",
|
||||
_scene.ToString(true).c_str());
|
||||
vblog(LOG_INFO, "trigger screenshot of main output");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ static std::map<MacroActionVariable::Type, std::string> actionTypes = {
|
||||
"AdvSceneSwitcher.action.variable.type.setConditionValue"},
|
||||
{MacroActionVariable::Type::SET_ACTION_VALUE,
|
||||
"AdvSceneSwitcher.action.variable.type.setActionValue"},
|
||||
{MacroActionVariable::Type::ROUND_TO_INT,
|
||||
"AdvSceneSwitcher.action.variable.type.roundToInt"},
|
||||
{MacroActionVariable::Type::SUBSTRING,
|
||||
"AdvSceneSwitcher.action.variable.type.subString"},
|
||||
};
|
||||
|
||||
static void apppend(Variable &var, const std::string &value)
|
||||
@@ -51,6 +55,46 @@ MacroActionVariable::~MacroActionVariable()
|
||||
DecrementCurrentSegmentVariableRef();
|
||||
}
|
||||
|
||||
void MacroActionVariable::HandleIndexSubString(Variable *var)
|
||||
{
|
||||
const auto curValue = var->Value();
|
||||
try {
|
||||
if (_subStringSize == 0) {
|
||||
var->SetValue(curValue.substr(_subStringStart));
|
||||
return;
|
||||
}
|
||||
var->SetValue(curValue.substr(_subStringStart, _subStringSize));
|
||||
} catch (const std::out_of_range &) {
|
||||
vblog(LOG_WARNING,
|
||||
"invalid start index \"%d\" selected for substring of \"%s\" of variable \"%s\"",
|
||||
_subStringStart, curValue.c_str(), var->Name().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void MacroActionVariable::HandleRegexSubString(Variable *var)
|
||||
{
|
||||
const auto curValue = var->Value();
|
||||
auto regex = _regex.GetRegularExpression(_regexPattern);
|
||||
if (!regex.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = regex.globalMatch(QString::fromStdString(curValue));
|
||||
for (int idx = 0; idx < _regexMatchIdx; idx++) {
|
||||
if (!it.hasNext()) {
|
||||
return;
|
||||
}
|
||||
it.next();
|
||||
}
|
||||
|
||||
if (!it.hasNext()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto match = it.next();
|
||||
var->SetValue(match.captured(0).toStdString());
|
||||
}
|
||||
|
||||
bool MacroActionVariable::PerformAction()
|
||||
{
|
||||
auto var = GetVariableByName(_variableName);
|
||||
@@ -95,6 +139,22 @@ bool MacroActionVariable::PerformAction()
|
||||
var->SetValue(segment->GetVariableValue());
|
||||
break;
|
||||
}
|
||||
case Type::ROUND_TO_INT: {
|
||||
double curValue;
|
||||
if (!var->DoubleValue(curValue)) {
|
||||
return true;
|
||||
}
|
||||
var->SetValue(std::to_string(int(std::round(curValue))));
|
||||
return true;
|
||||
}
|
||||
case Type::SUBSTRING: {
|
||||
if (_regex.Enabled()) {
|
||||
HandleRegexSubString(var);
|
||||
return true;
|
||||
}
|
||||
HandleIndexSubString(var);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -109,6 +169,11 @@ bool MacroActionVariable::Save(obs_data_t *obj) const
|
||||
obs_data_set_double(obj, "numValue", _numValue);
|
||||
obs_data_set_int(obj, "condition", static_cast<int>(_type));
|
||||
obs_data_set_int(obj, "segmentIdx", GetSegmentIndexValue());
|
||||
obs_data_set_int(obj, "subStringStart", _subStringStart);
|
||||
obs_data_set_int(obj, "subStringSize", _subStringSize);
|
||||
obs_data_set_string(obj, "regexPattern", _regexPattern.c_str());
|
||||
obs_data_set_int(obj, "regexMatchIdx", _regexMatchIdx);
|
||||
_regex.Save(obj);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -121,6 +186,11 @@ bool MacroActionVariable::Load(obs_data_t *obj)
|
||||
_numValue = obs_data_get_double(obj, "numValue");
|
||||
_type = static_cast<Type>(obs_data_get_int(obj, "condition"));
|
||||
_segmentIdxLoadValue = obs_data_get_int(obj, "segmentIdx");
|
||||
_subStringStart = obs_data_get_int(obj, "subStringStart");
|
||||
_subStringSize = obs_data_get_int(obj, "subStringSize");
|
||||
_regex.Load(obj);
|
||||
_regexPattern = obs_data_get_string(obj, "regexPattern");
|
||||
_regexMatchIdx = obs_data_get_int(obj, "regexMatchIdx");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -152,11 +222,11 @@ void MacroActionVariable::SetSegmentIndexValue(int value)
|
||||
|
||||
std::shared_ptr<MacroSegment> segment;
|
||||
if (_type == Type::SET_CONDITION_VALUE) {
|
||||
if (value < m->Conditions().size()) {
|
||||
if (value < (int)m->Conditions().size()) {
|
||||
segment = m->Conditions().at(value);
|
||||
}
|
||||
} else if (_type == Type::SET_ACTION_VALUE) {
|
||||
if (value < m->Actions().size()) {
|
||||
if (value < (int)m->Actions().size()) {
|
||||
segment = m->Actions().at(value);
|
||||
}
|
||||
}
|
||||
@@ -225,7 +295,15 @@ MacroActionVariableEdit::MacroActionVariableEdit(
|
||||
_numValue(new QDoubleSpinBox()),
|
||||
_segmentIdx(new QSpinBox()),
|
||||
_segmentValueStatus(new QLabel()),
|
||||
_segmentValue(new ResizingPlainTextEdit(this, 10, 1, 1))
|
||||
_segmentValue(new ResizingPlainTextEdit(this, 10, 1, 1)),
|
||||
_substringLayout(new QVBoxLayout()),
|
||||
_subStringIndexEntryLayout(new QHBoxLayout()),
|
||||
_subStringRegexEntryLayout(new QHBoxLayout()),
|
||||
_subStringStart(new QSpinBox()),
|
||||
_subStringSize(new QSpinBox()),
|
||||
_regex(new RegexConfigWidget(parent)),
|
||||
_regexPattern(new ResizingPlainTextEdit(this, 10, 1, 1)),
|
||||
_regexMatchIdx(new QSpinBox())
|
||||
{
|
||||
_numValue->setMinimum(-9999999999);
|
||||
_numValue->setMaximum(9999999999);
|
||||
@@ -233,6 +311,17 @@ MacroActionVariableEdit::MacroActionVariableEdit(
|
||||
_segmentIdx->setMaximum(99);
|
||||
_segmentIdx->setSpecialValueText("-");
|
||||
_segmentValue->setReadOnly(true);
|
||||
_subStringStart->setMinimum(1);
|
||||
_subStringStart->setMaximum(99999);
|
||||
_subStringStart->setSpecialValueText(obs_module_text(
|
||||
"AdvSceneSwitcher.action.variable.subString.begin"));
|
||||
_subStringSize->setMinimum(0);
|
||||
_subStringSize->setMaximum(99999);
|
||||
_subStringSize->setSpecialValueText(obs_module_text(
|
||||
"AdvSceneSwitcher.action.variable.subString.all"));
|
||||
_regexMatchIdx->setMinimum(1);
|
||||
_regexMatchIdx->setMaximum(999);
|
||||
_regexMatchIdx->setSuffix(".");
|
||||
populateTypeSelection(_actions);
|
||||
|
||||
QWidget::connect(_variables, SIGNAL(SelectionChanged(const QString &)),
|
||||
@@ -249,6 +338,16 @@ MacroActionVariableEdit::MacroActionVariableEdit(
|
||||
SLOT(SegmentIndexChanged(int)));
|
||||
QWidget::connect(window(), SIGNAL(MacroSegmentOrderChanged()), this,
|
||||
SLOT(MacroSegmentOrderChanged()));
|
||||
QWidget::connect(_subStringStart, SIGNAL(valueChanged(int)), this,
|
||||
SLOT(SubStringStartChanged(int)));
|
||||
QWidget::connect(_subStringSize, SIGNAL(valueChanged(int)), this,
|
||||
SLOT(SubStringSizeChanged(int)));
|
||||
QWidget::connect(_regex, SIGNAL(RegexConfigChanged(RegexConfig)), this,
|
||||
SLOT(RegexChanged(RegexConfig)));
|
||||
QWidget::connect(_regexPattern, SIGNAL(textChanged()), this,
|
||||
SLOT(RegexPatternChanged()));
|
||||
QWidget::connect(_regexMatchIdx, SIGNAL(valueChanged(int)), this,
|
||||
SLOT(RegexMatchIdxChanged(int)));
|
||||
|
||||
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{variables}}", _variables},
|
||||
@@ -257,13 +356,36 @@ MacroActionVariableEdit::MacroActionVariableEdit(
|
||||
{"{{strValue}}", _strValue},
|
||||
{"{{numValue}}", _numValue},
|
||||
{"{{segmentIndex}}", _segmentIdx},
|
||||
{"{{subStringStart}}", _subStringStart},
|
||||
{"{{subStringSize}}", _subStringSize},
|
||||
{"{{regexMatchIdx}}", _regexMatchIdx},
|
||||
};
|
||||
auto entryLayout = new QHBoxLayout;
|
||||
placeWidgets(obs_module_text("AdvSceneSwitcher.action.variable.entry"),
|
||||
entryLayout, widgetPlaceholders);
|
||||
|
||||
placeWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.variable.entry.substringIndex"),
|
||||
_subStringIndexEntryLayout, widgetPlaceholders);
|
||||
|
||||
placeWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.variable.entry.substringRegex"),
|
||||
_subStringRegexEntryLayout, widgetPlaceholders);
|
||||
|
||||
auto regexConfigLayout = new QHBoxLayout;
|
||||
regexConfigLayout->addWidget(_regex);
|
||||
regexConfigLayout->addStretch();
|
||||
|
||||
_substringLayout->addLayout(_subStringIndexEntryLayout);
|
||||
_substringLayout->addLayout(_subStringRegexEntryLayout);
|
||||
_substringLayout->addWidget(_regexPattern);
|
||||
_substringLayout->addLayout(regexConfigLayout);
|
||||
|
||||
auto layout = new QVBoxLayout;
|
||||
layout->addLayout(entryLayout);
|
||||
layout->addLayout(_substringLayout);
|
||||
layout->addWidget(_segmentValueStatus);
|
||||
layout->addWidget(_segmentValue);
|
||||
setLayout(layout);
|
||||
@@ -290,6 +412,12 @@ void MacroActionVariableEdit::UpdateEntryData()
|
||||
_strValue->setPlainText(QString::fromStdString(_entryData->_strValue));
|
||||
_numValue->setValue(_entryData->_numValue);
|
||||
_segmentIdx->setValue(_entryData->GetSegmentIndexValue() + 1);
|
||||
_subStringStart->setValue(_entryData->_subStringStart + 1);
|
||||
_subStringSize->setValue(_entryData->_subStringSize);
|
||||
_regex->SetRegexConfig(_entryData->_regex);
|
||||
_regexPattern->setPlainText(
|
||||
QString::fromStdString(_entryData->_regexPattern));
|
||||
_regexMatchIdx->setValue(_entryData->_regexMatchIdx + 1);
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
@@ -399,13 +527,13 @@ void MacroActionVariableEdit::UpdateSegmentVariableValue()
|
||||
std::shared_ptr<MacroSegment> segment;
|
||||
if (_entryData->_type == MacroActionVariable::Type::SET_ACTION_VALUE) {
|
||||
const auto &actions = m->Actions();
|
||||
if (index < actions.size()) {
|
||||
if (index < (int)actions.size()) {
|
||||
segment = actions.at(index);
|
||||
}
|
||||
} else if (_entryData->_type ==
|
||||
MacroActionVariable::Type::SET_CONDITION_VALUE) {
|
||||
const auto &conditions = m->Conditions();
|
||||
if (index < conditions.size()) {
|
||||
if (index < (int)conditions.size()) {
|
||||
segment = conditions.at(index);
|
||||
}
|
||||
}
|
||||
@@ -454,6 +582,59 @@ void MacroActionVariableEdit::MacroSegmentOrderChanged()
|
||||
_segmentIdx->setValue(_entryData->GetSegmentIndexValue() + 1);
|
||||
}
|
||||
|
||||
void MacroActionVariableEdit::SubStringStartChanged(int val)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
_entryData->_subStringStart = val - 1;
|
||||
}
|
||||
|
||||
void MacroActionVariableEdit::SubStringSizeChanged(int val)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
_entryData->_subStringSize = val;
|
||||
}
|
||||
|
||||
void MacroActionVariableEdit::RegexChanged(RegexConfig conf)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
_entryData->_regex = conf;
|
||||
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroActionVariableEdit::RegexPatternChanged()
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
_entryData->_regexPattern = _regexPattern->toPlainText().toStdString();
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
void MacroActionVariableEdit::RegexMatchIdxChanged(int val)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
_entryData->_regexMatchIdx = val - 1;
|
||||
}
|
||||
|
||||
void MacroActionVariableEdit::MarkSelectedSegment()
|
||||
{
|
||||
if (switcher->disableHints) {
|
||||
@@ -472,14 +653,14 @@ void MacroActionVariableEdit::MarkSelectedSegment()
|
||||
|
||||
if (_entryData->_type == MacroActionVariable::Type::SET_ACTION_VALUE) {
|
||||
const auto &actions = m->Actions();
|
||||
if (index >= actions.size()) {
|
||||
if (index >= (int)actions.size()) {
|
||||
return;
|
||||
}
|
||||
AdvSceneSwitcher::window->HighlightAction(
|
||||
index, QColor(Qt::lightGray));
|
||||
} else {
|
||||
const auto &conditions = m->Conditions();
|
||||
if (index >= conditions.size()) {
|
||||
if (index >= (int)conditions.size()) {
|
||||
return;
|
||||
}
|
||||
AdvSceneSwitcher::window->HighlightCondition(
|
||||
@@ -520,6 +701,15 @@ void MacroActionVariableEdit::SetWidgetVisibility()
|
||||
MacroActionVariable::Type::SET_ACTION_VALUE ||
|
||||
_entryData->_type ==
|
||||
MacroActionVariable::Type::SET_CONDITION_VALUE);
|
||||
setLayoutVisible(_substringLayout,
|
||||
_entryData->_type ==
|
||||
MacroActionVariable::Type::SUBSTRING);
|
||||
if (_entryData->_type == MacroActionVariable::Type::SUBSTRING) {
|
||||
bool showRegex = _entryData->_regex.Enabled();
|
||||
setLayoutVisible(_subStringIndexEntryLayout, !showRegex);
|
||||
setLayoutVisible(_subStringRegexEntryLayout, showRegex);
|
||||
_regexPattern->setVisible(showRegex);
|
||||
}
|
||||
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "resizing-text-edit.hpp"
|
||||
#include "regex-config.hpp"
|
||||
#include "variable.hpp"
|
||||
|
||||
class MacroActionVariable : public MacroAction {
|
||||
@@ -28,6 +29,8 @@ public:
|
||||
DECREMENT,
|
||||
SET_CONDITION_VALUE,
|
||||
SET_ACTION_VALUE,
|
||||
ROUND_TO_INT,
|
||||
SUBSTRING,
|
||||
};
|
||||
|
||||
Type _type = Type::SET_FIXED_VALUE;
|
||||
@@ -35,9 +38,16 @@ public:
|
||||
std::string _variable2Name = "";
|
||||
std::string _strValue = "";
|
||||
double _numValue = 0;
|
||||
int _subStringStart = 0;
|
||||
int _subStringSize = 0;
|
||||
RegexConfig _regex = RegexConfig::PartialMatchRegexConfig();
|
||||
std::string _regexPattern = ".*";
|
||||
int _regexMatchIdx = 0;
|
||||
|
||||
private:
|
||||
void DecrementCurrentSegmentVariableRef();
|
||||
void HandleIndexSubString(Variable *);
|
||||
void HandleRegexSubString(Variable *);
|
||||
|
||||
std::weak_ptr<MacroSegment> _macroSegment;
|
||||
int _segmentIdxLoadValue = -1;
|
||||
@@ -70,6 +80,11 @@ private slots:
|
||||
void SegmentIndexChanged(int val);
|
||||
void UpdateSegmentVariableValue();
|
||||
void MacroSegmentOrderChanged();
|
||||
void SubStringStartChanged(int val);
|
||||
void SubStringSizeChanged(int val);
|
||||
void RegexChanged(RegexConfig conf);
|
||||
void RegexPatternChanged();
|
||||
void RegexMatchIdxChanged(int val);
|
||||
|
||||
signals:
|
||||
void HeaderInfoChanged(const QString &);
|
||||
@@ -79,10 +94,18 @@ protected:
|
||||
VariableSelection *_variables2;
|
||||
QComboBox *_actions;
|
||||
ResizingPlainTextEdit *_strValue;
|
||||
QSpinBox *_segmentIdx;
|
||||
QDoubleSpinBox *_numValue;
|
||||
QSpinBox *_segmentIdx;
|
||||
QLabel *_segmentValueStatus;
|
||||
ResizingPlainTextEdit *_segmentValue;
|
||||
QVBoxLayout *_substringLayout;
|
||||
QHBoxLayout *_subStringIndexEntryLayout;
|
||||
QHBoxLayout *_subStringRegexEntryLayout;
|
||||
QSpinBox *_subStringStart;
|
||||
QSpinBox *_subStringSize;
|
||||
RegexConfigWidget *_regex;
|
||||
ResizingPlainTextEdit *_regexPattern;
|
||||
QSpinBox *_regexMatchIdx;
|
||||
std::shared_ptr<MacroActionVariable> _entryData;
|
||||
|
||||
private:
|
||||
|
||||
@@ -19,6 +19,17 @@ static std::map<WaitType, std::string> waitTypes = {
|
||||
static std::random_device rd;
|
||||
static std::default_random_engine re(rd());
|
||||
|
||||
static void waitHelper(std::unique_lock<std::mutex> *lock, Macro *macro,
|
||||
std::chrono::high_resolution_clock::time_point &time)
|
||||
{
|
||||
while (!switcher->abortMacroWait && !macro->GetStop()) {
|
||||
if (switcher->macroWaitCv.wait_until(*lock, time) ==
|
||||
std::cv_status::timeout) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MacroActionWait::PerformAction()
|
||||
{
|
||||
double sleepDuration;
|
||||
@@ -39,15 +50,10 @@ bool MacroActionWait::PerformAction()
|
||||
|
||||
auto time = std::chrono::high_resolution_clock::now() +
|
||||
std::chrono::milliseconds((int)(sleepDuration * 1000));
|
||||
auto macro = GetMacro();
|
||||
|
||||
switcher->abortMacroWait = false;
|
||||
std::unique_lock<std::mutex> lock(switcher->m);
|
||||
while (!switcher->abortMacroWait && !macro->GetStop()) {
|
||||
if (switcher->macroWaitCv.wait_until(lock, time) ==
|
||||
std::cv_status::timeout) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
waitHelper(&lock, GetMacro(), time);
|
||||
|
||||
return !switcher->abortMacroWait;
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ void AdvSceneSwitcher::AddMacroCondition(int idx)
|
||||
if (idx >= 1) {
|
||||
id = macro->Conditions().at(idx - 1)->GetId();
|
||||
if (idx == 1) {
|
||||
logic = LogicType::NONE;
|
||||
logic = LogicType::OR;
|
||||
} else {
|
||||
logic = macro->Conditions().at(idx - 1)->GetLogicType();
|
||||
}
|
||||
|
||||
@@ -401,9 +401,9 @@ static void populateSourceTypes(QComboBox *list)
|
||||
MacroConditionMediaEdit::MacroConditionMediaEdit(
|
||||
QWidget *parent, std::shared_ptr<MacroConditionMedia> entryData)
|
||||
: QWidget(parent),
|
||||
_sourceTypes(new QComboBox()),
|
||||
_scenes(new SceneSelectionWidget(window(), true, true, true, true,
|
||||
true)),
|
||||
_sourceTypes(new QComboBox()),
|
||||
_sources(new SourceSelectionWidget(this, QStringList(), true)),
|
||||
_states(new QComboBox()),
|
||||
_timeRestrictions(new QComboBox()),
|
||||
|
||||
@@ -53,7 +53,7 @@ bool MacroConditionScene::CheckCondition()
|
||||
auto current = obs_frontend_get_current_scene();
|
||||
auto weak = obs_source_get_weak_source(current);
|
||||
bool match = weak == _scene.GetScene(false);
|
||||
SetVariableValue(obs_source_get_name(current));
|
||||
SetVariableValue(GetWeakSourceName(weak));
|
||||
obs_weak_source_release(weak);
|
||||
obs_source_release(current);
|
||||
return match;
|
||||
|
||||
@@ -96,6 +96,10 @@ bool MacroConditionVariable::CompareVariables()
|
||||
return validNumbers && val1 < val2;
|
||||
case MacroConditionVariable::Type::GREATER_THAN_VARIABLE:
|
||||
return validNumbers && val1 > val2;
|
||||
default:
|
||||
blog(LOG_WARNING,
|
||||
"Unexpected call of %s with condition type %d", __func__,
|
||||
static_cast<int>(_type));
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -75,7 +75,7 @@ void MacroSegmentList::SetHelpMsgVisible(bool visible)
|
||||
_helpMsg->setVisible(visible);
|
||||
}
|
||||
|
||||
void MacroSegmentList::Insert(int idx, QWidget *widget)
|
||||
void MacroSegmentList::Insert(int idx, MacroSegmentEdit *widget)
|
||||
{
|
||||
widget->installEventFilter(this);
|
||||
_contentLayout->insertWidget(idx, widget);
|
||||
@@ -135,8 +135,6 @@ void MacroSegmentList::SetSelection(int idx)
|
||||
|
||||
bool MacroSegmentList::eventFilter(QObject *object, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::MouseButtonPress) {
|
||||
}
|
||||
switch (event->type()) {
|
||||
case QEvent::MouseButtonPress:
|
||||
mousePressEvent(static_cast<QMouseEvent *>(event));
|
||||
|
||||
@@ -16,7 +16,7 @@ public:
|
||||
void SetHelpMsg(const QString &msg);
|
||||
void SetHelpMsgVisible(bool visible);
|
||||
MacroSegmentEdit *WidgetAt(int idx);
|
||||
void Insert(int idx, QWidget *widget);
|
||||
void Insert(int idx, MacroSegmentEdit *widget);
|
||||
void Add(QWidget *widget);
|
||||
void Remove(int idx);
|
||||
void Clear(int idx = 0); // Clear all elements >= idx
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
#include <QMouseEvent>
|
||||
#include <QLabel>
|
||||
#include <QScrollBar>
|
||||
#include <QApplication>
|
||||
|
||||
MacroSegment::MacroSegment(Macro *m, bool supportsVariableValue)
|
||||
: _supportsVariableValue(supportsVariableValue), _macro(m)
|
||||
: _macro(m), _supportsVariableValue(supportsVariableValue)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -182,11 +183,37 @@ MacroSegmentEdit::MacroSegmentEdit(bool highlight, QWidget *parent)
|
||||
SetSelected(false);
|
||||
ShowDropLine(DropLineState::NONE);
|
||||
|
||||
// Enable dragging while clicking on the header text
|
||||
_headerInfo->installEventFilter(this);
|
||||
|
||||
_timer.setInterval(1500);
|
||||
connect(&_timer, SIGNAL(timeout()), this, SLOT(Highlight()));
|
||||
_timer.start();
|
||||
}
|
||||
|
||||
bool MacroSegmentEdit::eventFilter(QObject *obj, QEvent *ev)
|
||||
{
|
||||
if (obj == _headerInfo && ev->type() == QEvent::MouseMove) {
|
||||
if (!parentWidget()) {
|
||||
return QWidget::eventFilter(obj, ev);
|
||||
}
|
||||
// Generate a mouse move event for the MacroSegmentList
|
||||
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(ev);
|
||||
QMouseEvent *newEvent = new QMouseEvent(
|
||||
mouseEvent->type(),
|
||||
_headerInfo->mapTo(this, mouseEvent->pos()),
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
mouseEvent->globalPos(),
|
||||
#else
|
||||
mouseEvent->globalPosition(),
|
||||
#endif
|
||||
mouseEvent->button(), mouseEvent->buttons(),
|
||||
mouseEvent->modifiers());
|
||||
QApplication::sendEvent(parentWidget(), newEvent);
|
||||
}
|
||||
return QWidget::eventFilter(obj, ev);
|
||||
}
|
||||
|
||||
void MacroSegmentEdit::HeaderInfoChanged(const QString &text)
|
||||
{
|
||||
_headerInfo->setVisible(!text.isEmpty());
|
||||
|
||||
@@ -75,6 +75,8 @@ signals:
|
||||
void SceneGroupRenamed(const QString &oldName, const QString newName);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *obj, QEvent *ev) override;
|
||||
|
||||
Section *_section;
|
||||
QLabel *_headerInfo;
|
||||
QWidget *_frame;
|
||||
|
||||
@@ -83,14 +83,14 @@ void AdvSceneSwitcher::on_macroAdd_clicked()
|
||||
emit MacroAdded(QString::fromStdString(name));
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::on_macroRemove_clicked()
|
||||
void AdvSceneSwitcher::RemoveMacro(std::shared_ptr<Macro> ¯o)
|
||||
{
|
||||
auto macro = getSelectedMacro();
|
||||
if (!macro) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto name = QString::fromStdString(macro->Name());
|
||||
if (macro->IsGroup()) {
|
||||
if (macro->IsGroup() && macro->GroupSize() > 0) {
|
||||
QString deleteWarning = obs_module_text(
|
||||
"AdvSceneSwitcher.macroTab.groupDeleteConfirm");
|
||||
if (!DisplayMessage(deleteWarning.arg(name), true)) {
|
||||
@@ -106,6 +106,40 @@ void AdvSceneSwitcher::on_macroRemove_clicked()
|
||||
emit MacroRemoved(name);
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::RenameMacro(std::shared_ptr<Macro> ¯o,
|
||||
const QString &name)
|
||||
{
|
||||
auto oldName = QString::fromStdString(macro->Name());
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
macro->SetName(name.toStdString());
|
||||
}
|
||||
emit MacroRenamed(oldName, name);
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::on_macroRemove_clicked()
|
||||
{
|
||||
auto macros = getSelectedMacros();
|
||||
if (macros.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (macros.size() == 1) {
|
||||
RemoveMacro(macros.at(0));
|
||||
return;
|
||||
}
|
||||
|
||||
QString deleteWarning = obs_module_text(
|
||||
"AdvSceneSwitcher.macroTab.deleteMultipleMacrosConfirmation");
|
||||
if (!DisplayMessage(deleteWarning.arg(macros.size()), true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto ¯o : macros) {
|
||||
RemoveMacro(macro);
|
||||
}
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::on_macroUp_clicked()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
@@ -126,10 +160,37 @@ void AdvSceneSwitcher::on_macroDown_clicked()
|
||||
ui->macros->Down(macro);
|
||||
}
|
||||
|
||||
static bool newMacroNameValid(const std::string &name)
|
||||
{
|
||||
if (!macroNameExists(name)) {
|
||||
return true;
|
||||
}
|
||||
DisplayMessage(obs_module_text("AdvSceneSwitcher.macroTab.exists"));
|
||||
return false;
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::RenameCurrentMacro()
|
||||
{
|
||||
auto macro = getSelectedMacro();
|
||||
if (!macro) {
|
||||
return;
|
||||
}
|
||||
std::string oldName = macro->Name();
|
||||
std::string name;
|
||||
if (!AdvSSNameDialog::AskForName(
|
||||
this, obs_module_text("AdvSceneSwitcher.windowTitle"),
|
||||
obs_module_text("AdvSceneSwitcher.item.newName"), name,
|
||||
QString::fromStdString(oldName))) {
|
||||
return;
|
||||
}
|
||||
if (name.empty() || name == oldName || !newMacroNameValid(name)) {
|
||||
return;
|
||||
}
|
||||
RenameMacro(macro, QString::fromStdString(name));
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::on_macroName_editingFinished()
|
||||
{
|
||||
bool nameValid = true;
|
||||
|
||||
auto macro = getSelectedMacro();
|
||||
if (!macro) {
|
||||
return;
|
||||
@@ -138,33 +199,12 @@ void AdvSceneSwitcher::on_macroName_editingFinished()
|
||||
QString newName = ui->macroName->text();
|
||||
QString oldName = QString::fromStdString(macro->Name());
|
||||
|
||||
if (newName.isEmpty() || newName == oldName) {
|
||||
nameValid = false;
|
||||
}
|
||||
|
||||
if (nameValid && macroNameExists(newName.toUtf8().constData())) {
|
||||
DisplayMessage(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.exists"));
|
||||
nameValid = false;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
if (nameValid) {
|
||||
macro->SetName(newName.toUtf8().constData());
|
||||
auto macro = getSelectedMacro();
|
||||
if (!macro) {
|
||||
return;
|
||||
}
|
||||
macro->SetName(newName.toStdString());
|
||||
} else {
|
||||
ui->macroName->setText(oldName);
|
||||
}
|
||||
}
|
||||
|
||||
if (nameValid) {
|
||||
emit MacroRenamed(oldName, newName);
|
||||
if (newName.isEmpty() || newName == oldName ||
|
||||
!newMacroNameValid(newName.toStdString())) {
|
||||
ui->macroName->setText(oldName);
|
||||
return;
|
||||
}
|
||||
RenameMacro(macro, newName);
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::on_runMacro_clicked()
|
||||
@@ -314,6 +354,11 @@ std::shared_ptr<Macro> AdvSceneSwitcher::getSelectedMacro()
|
||||
return ui->macros->GetCurrentMacro();
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<Macro>> AdvSceneSwitcher::getSelectedMacros()
|
||||
{
|
||||
return ui->macros->GetCurrentMacros();
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::MacroSelectionChanged(const QItemSelection &,
|
||||
const QItemSelection &)
|
||||
{
|
||||
@@ -335,7 +380,8 @@ void AdvSceneSwitcher::MacroSelectionChanged(const QItemSelection &,
|
||||
|
||||
void AdvSceneSwitcher::HighlightOnChange()
|
||||
{
|
||||
if (!switcher->macroProperties._highlightExecuted) {
|
||||
if (!switcher->macroProperties._highlightActions &&
|
||||
!switcher->macroProperties._highlightExecuted) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -466,15 +512,20 @@ void AdvSceneSwitcher::ShowMacroContextMenu(const QPoint &pos)
|
||||
{
|
||||
QPoint globalPos = ui->macros->mapToGlobal(pos);
|
||||
QMenu menu;
|
||||
|
||||
auto add = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.contextMenuAdd"),
|
||||
this, &AdvSceneSwitcher::on_macroAdd_clicked);
|
||||
|
||||
auto copy = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.copy"), this,
|
||||
&AdvSceneSwitcher::CopyMacro);
|
||||
copy->setDisabled(ui->macros->GroupsSelected());
|
||||
copy->setEnabled(ui->macros->SingleItemSelected());
|
||||
menu.addSeparator();
|
||||
|
||||
auto group = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.group"), ui->macros,
|
||||
&MacroTree::GroupSelectedItems);
|
||||
// Nested groups are not supported
|
||||
group->setDisabled(ui->macros->GroupedItemsSelected() ||
|
||||
ui->macros->GroupsSelected() ||
|
||||
ui->macros->SelectionEmpty());
|
||||
@@ -483,6 +534,17 @@ void AdvSceneSwitcher::ShowMacroContextMenu(const QPoint &pos)
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.ungroup"),
|
||||
ui->macros, &MacroTree::UngroupSelectedGroups);
|
||||
ungroup->setEnabled(ui->macros->GroupsSelected());
|
||||
menu.addSeparator();
|
||||
|
||||
auto rename = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.rename"), this,
|
||||
&AdvSceneSwitcher::RenameCurrentMacro);
|
||||
rename->setEnabled(ui->macros->SingleItemSelected());
|
||||
|
||||
auto remove = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.remove"), this,
|
||||
&AdvSceneSwitcher::on_macroRemove_clicked);
|
||||
remove->setDisabled(ui->macros->SelectionEmpty());
|
||||
|
||||
menu.exec(globalPos);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "macro-tree.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "switcher-data-structs.hpp"
|
||||
|
||||
#include <obs.h>
|
||||
#include <string>
|
||||
@@ -18,6 +19,8 @@ MacroTreeItem::MacroTreeItem(MacroTree *tree, std::shared_ptr<Macro> macroItem,
|
||||
: _tree(tree), _highlight(highlight), _macro(macroItem)
|
||||
{
|
||||
setAttribute(Qt::WA_TranslucentBackground);
|
||||
// This is necessary for some theses to display active selections properly
|
||||
setStyleSheet("background: none");
|
||||
|
||||
auto name = _macro->Name();
|
||||
bool macroPaused = _macro->Paused();
|
||||
@@ -372,24 +375,26 @@ void MacroTreeModel::Add(std::shared_ptr<Macro> item)
|
||||
|
||||
void MacroTreeModel::Remove(std::shared_ptr<Macro> item)
|
||||
{
|
||||
auto startIdx = GetItemModelIndex(item);
|
||||
if (startIdx == -1) {
|
||||
auto uiStartIdx = GetItemModelIndex(item);
|
||||
if (uiStartIdx == -1) {
|
||||
return;
|
||||
}
|
||||
auto macroStartIdx = ModelIndexToMacroIndex(startIdx, _macros);
|
||||
auto macroStartIdx = ModelIndexToMacroIndex(uiStartIdx, _macros);
|
||||
|
||||
auto endIdx = startIdx;
|
||||
auto uiEndIdx = uiStartIdx;
|
||||
auto macroEndIdx = macroStartIdx;
|
||||
|
||||
bool isGroup = item->IsGroup();
|
||||
if (isGroup) {
|
||||
macroEndIdx += item->GroupSize();
|
||||
if (!item->IsCollapsed()) {
|
||||
endIdx = item->GroupSize();
|
||||
uiEndIdx += item->GroupSize();
|
||||
}
|
||||
} else if (item->IsSubitem()) {
|
||||
Macro::PrepareMoveToGroup(nullptr, item);
|
||||
}
|
||||
|
||||
beginRemoveRows(QModelIndex(), startIdx, endIdx);
|
||||
beginRemoveRows(QModelIndex(), uiStartIdx, uiEndIdx);
|
||||
_macros.erase(std::next(_macros.begin(), macroStartIdx),
|
||||
std::next(_macros.begin(), macroEndIdx + 1));
|
||||
endRemoveRows();
|
||||
@@ -469,6 +474,21 @@ std::shared_ptr<Macro> MacroTreeModel::GetCurrentMacro() const
|
||||
return _macros[ModelIndexToMacroIndex(idx, _macros)];
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<Macro>>
|
||||
MacroTreeModel::GetCurrentMacros(const QModelIndexList &selection) const
|
||||
{
|
||||
std::vector<std::shared_ptr<Macro>> result;
|
||||
result.reserve(selection.size());
|
||||
for (const auto &sel : selection) {
|
||||
try {
|
||||
result.emplace_back(_macros.at(
|
||||
ModelIndexToMacroIndex(sel.row(), _macros)));
|
||||
} catch (const std::out_of_range &) {
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
MacroTreeModel::MacroTreeModel(MacroTree *st_,
|
||||
std::deque<std::shared_ptr<Macro>> ¯os)
|
||||
: QAbstractListModel(st_), _mt(st_), _macros(macros)
|
||||
@@ -561,38 +581,44 @@ void MacroTreeModel::GroupSelectedItems(QModelIndexList &indices)
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
QString name = GetNewGroupName();
|
||||
std::vector<std::shared_ptr<Macro>> items;
|
||||
items.reserve(indices.size());
|
||||
std::vector<std::shared_ptr<Macro>> itemsToGroup;
|
||||
itemsToGroup.reserve(indices.size());
|
||||
int insertGroupAt = indices[0].row();
|
||||
for (int i = 0; i < indices.count(); i++) {
|
||||
std::shared_ptr<Macro> item = _macros[ModelIndexToMacroIndex(
|
||||
indices[i].row(), _macros)];
|
||||
items.emplace_back(item);
|
||||
int selectedIdx = indices[i].row();
|
||||
if (selectedIdx < insertGroupAt) {
|
||||
insertGroupAt = selectedIdx;
|
||||
}
|
||||
|
||||
std::shared_ptr<Macro> item =
|
||||
_macros[ModelIndexToMacroIndex(selectedIdx, _macros)];
|
||||
itemsToGroup.emplace_back(item);
|
||||
}
|
||||
|
||||
std::shared_ptr<Macro> item =
|
||||
Macro::CreateGroup(name.toStdString(), items);
|
||||
Macro::CreateGroup(name.toStdString(), itemsToGroup);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A new list entry for group
|
||||
Add(item);
|
||||
_macros.insert(_macros.begin() + insertGroupAt, item);
|
||||
|
||||
// Move all selected items after new group entry
|
||||
for (const auto &item : items) {
|
||||
int offset = 1;
|
||||
for (const auto &item : itemsToGroup) {
|
||||
auto it = _macros.begin() + GetItemMacroIndex(item);
|
||||
std::rotate(it, std::next(it, 1), _macros.end());
|
||||
_macros.erase(it);
|
||||
_macros.insert(_macros.begin() + insertGroupAt + offset, item);
|
||||
offset++;
|
||||
}
|
||||
|
||||
_hasGroups = true;
|
||||
_mt->selectionModel()->clear();
|
||||
|
||||
for (auto &m : _macros) {
|
||||
m->ResolveMacroRef();
|
||||
}
|
||||
|
||||
Reset(_macros);
|
||||
|
||||
for (auto &m : _macros) {
|
||||
m->ResolveMacroRef();
|
||||
}
|
||||
@@ -604,6 +630,7 @@ void MacroTreeModel::UngroupSelectedGroups(QModelIndexList &indices)
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
for (int i = indices.count() - 1; i >= 0; i--) {
|
||||
std::shared_ptr<Macro> item = _macros[ModelIndexToMacroIndex(
|
||||
indices[i].row(), _macros)];
|
||||
@@ -698,6 +725,12 @@ std::shared_ptr<Macro> MacroTree::GetCurrentMacro() const
|
||||
return GetModel()->GetCurrentMacro();
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<Macro>> MacroTree::GetCurrentMacros() const
|
||||
{
|
||||
QModelIndexList indices = selectedIndexes();
|
||||
return GetModel()->GetCurrentMacros(indices);
|
||||
}
|
||||
|
||||
MacroTree::MacroTree(QWidget *parent_) : QListView(parent_)
|
||||
{
|
||||
setStyleSheet(QString(
|
||||
@@ -964,6 +997,7 @@ void MacroTree::dropEvent(QDropEvent *event)
|
||||
}
|
||||
|
||||
// Move items in backend
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
int to = row;
|
||||
try {
|
||||
to = ModelIndexToMacroIndex(row, items);
|
||||
@@ -1023,14 +1057,12 @@ void MacroTree::dropEvent(QDropEvent *event)
|
||||
|
||||
bool MacroTree::GroupsSelected() const
|
||||
{
|
||||
MacroTreeModel *mtm = GetModel();
|
||||
QModelIndexList selectedIndices = selectedIndexes();
|
||||
|
||||
if (SelectionEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto &idx : selectedIndices) {
|
||||
MacroTreeModel *mtm = GetModel();
|
||||
for (auto &idx : selectedIndexes()) {
|
||||
std::shared_ptr<Macro> item =
|
||||
mtm->_macros[ModelIndexToMacroIndex(idx.row(),
|
||||
mtm->_macros)];
|
||||
@@ -1043,14 +1075,12 @@ bool MacroTree::GroupsSelected() const
|
||||
|
||||
bool MacroTree::GroupedItemsSelected() const
|
||||
{
|
||||
MacroTreeModel *mtm = GetModel();
|
||||
QModelIndexList selectedIndices = selectedIndexes();
|
||||
|
||||
if (SelectionEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto &idx : selectedIndices) {
|
||||
auto *mtm = GetModel();
|
||||
for (auto &idx : selectedIndexes()) {
|
||||
std::shared_ptr<Macro> item =
|
||||
mtm->_macros[ModelIndexToMacroIndex(idx.row(),
|
||||
mtm->_macros)];
|
||||
@@ -1062,6 +1092,11 @@ bool MacroTree::GroupedItemsSelected() const
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MacroTree::SingleItemSelected() const
|
||||
{
|
||||
return selectedIndexes().size() == 1;
|
||||
}
|
||||
|
||||
bool MacroTree::SelectionEmpty() const
|
||||
{
|
||||
return selectedIndexes().empty();
|
||||
|
||||
@@ -92,6 +92,8 @@ private:
|
||||
std::shared_ptr<Macro> FindEndOfGroup(const std::shared_ptr<Macro> &m,
|
||||
bool above) const;
|
||||
std::shared_ptr<Macro> GetCurrentMacro() const;
|
||||
std::vector<std::shared_ptr<Macro>>
|
||||
GetCurrentMacros(const QModelIndexList &) const;
|
||||
QString GetNewGroupName();
|
||||
void GroupSelectedItems(QModelIndexList &indices);
|
||||
void UngroupSelectedGroups(QModelIndexList &indices);
|
||||
@@ -121,8 +123,10 @@ public:
|
||||
void Up(std::shared_ptr<Macro> item) const;
|
||||
void Down(std::shared_ptr<Macro> item) const;
|
||||
std::shared_ptr<Macro> GetCurrentMacro() const;
|
||||
std::vector<std::shared_ptr<Macro>> GetCurrentMacros() const;
|
||||
bool GroupsSelected() const;
|
||||
bool GroupedItemsSelected() const;
|
||||
bool SingleItemSelected() const;
|
||||
bool SelectionEmpty() const;
|
||||
|
||||
public slots:
|
||||
|
||||
@@ -34,7 +34,7 @@ Macro::CreateGroup(const std::string &name,
|
||||
{
|
||||
auto group = std::make_shared<Macro>(name, false);
|
||||
for (auto &c : children) {
|
||||
c->SetParent(group.get());
|
||||
c->SetParent(group);
|
||||
}
|
||||
group->_isGroup = true;
|
||||
group->_groupSize = children.size();
|
||||
@@ -82,7 +82,7 @@ void Macro::PrepareMoveToGroup(std::shared_ptr<Macro> group,
|
||||
oldGroup->_groupSize--;
|
||||
}
|
||||
|
||||
item->SetParent(group.get());
|
||||
item->SetParent(group);
|
||||
if (group) {
|
||||
group->_groupSize++;
|
||||
}
|
||||
@@ -204,8 +204,9 @@ bool Macro::PerformActions(bool forceParallel, bool ignorePause)
|
||||
RunActions(ret, ignorePause);
|
||||
}
|
||||
_wasExecutedRecently = true;
|
||||
if (_parent) {
|
||||
_parent->_wasExecutedRecently = true;
|
||||
auto group = _parent.lock();
|
||||
if (group) {
|
||||
group->_wasExecutedRecently = true;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -313,6 +314,12 @@ void Macro::UpdateConditionIndices()
|
||||
}
|
||||
}
|
||||
|
||||
Macro *Macro::Parent()
|
||||
{
|
||||
auto p = _parent.lock();
|
||||
return p.get();
|
||||
}
|
||||
|
||||
bool Macro::Save(obs_data_t *obj) const
|
||||
{
|
||||
obs_data_set_string(obj, "name", _name.c_str());
|
||||
@@ -709,18 +716,41 @@ void SwitcherData::loadMacros(obs_data_t *obj)
|
||||
obs_data_array_release(macroArray);
|
||||
|
||||
int groupCount = 0;
|
||||
Macro *group = nullptr;
|
||||
std::shared_ptr<Macro> group;
|
||||
std::vector<std::shared_ptr<Macro>> invalidGroups;
|
||||
for (auto &m : macros) {
|
||||
if (groupCount && m->IsGroup()) {
|
||||
blog(LOG_ERROR,
|
||||
"nested group detected - will delete \"%s\"",
|
||||
m->Name().c_str());
|
||||
invalidGroups.emplace_back(m);
|
||||
continue;
|
||||
}
|
||||
if (groupCount) {
|
||||
m->SetParent(group);
|
||||
groupCount--;
|
||||
}
|
||||
if (m->IsGroup()) {
|
||||
groupCount = m->GroupSize();
|
||||
group = m.get();
|
||||
group = m;
|
||||
}
|
||||
m->PostLoad();
|
||||
}
|
||||
|
||||
if (groupCount) {
|
||||
blog(LOG_ERROR,
|
||||
"invalid group size detected - will delete \"%s\"",
|
||||
group->Name().c_str());
|
||||
invalidGroups.emplace_back(group);
|
||||
}
|
||||
|
||||
for (auto &m : invalidGroups) {
|
||||
auto it = std::find(macros.begin(), macros.end(), m);
|
||||
if (it == macros.end()) {
|
||||
continue;
|
||||
}
|
||||
macros.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
bool SwitcherData::checkMacros()
|
||||
@@ -741,11 +771,28 @@ bool SwitcherData::checkMacros()
|
||||
|
||||
bool SwitcherData::runMacros()
|
||||
{
|
||||
// TODO: Don't rely on creating a copy of each macro once new frontend
|
||||
// events are available - see:
|
||||
// https://github.com/obsproject/obs-studio/commit/feda1aaa283e8a99f6ba1159cfe6b9c1f2934a61
|
||||
for (auto m : macros) {
|
||||
if (m->Matched()) {
|
||||
// Create copy of macor list as elements might be removed, inserted, or
|
||||
// reordered while macros are currently being executed.
|
||||
// For example, this can happen if a macro is performing a wait action,
|
||||
// as the main lock will be unlocked during this time.
|
||||
auto runPhaseMacros = macros;
|
||||
|
||||
// Avoid deadlocks when opening settings window and calling frontend
|
||||
// API functions at the same time.
|
||||
//
|
||||
// If the timing is just right, the frontend API call will call
|
||||
// QMetaObject::invokeMethod(...) with Qt::BlockingQueuedConnection
|
||||
// while holding the main switcher mutex.
|
||||
// But this invokeMethod call itself will be blocked as it is waiting
|
||||
// the constructor of AdvSceneSwitcher() to complete.
|
||||
// The constructor of AdvSceneSwitcher() cannot continue however as it
|
||||
// cannot lock the main switcher mutex.
|
||||
if (GetLock()) {
|
||||
GetLock()->unlock();
|
||||
}
|
||||
|
||||
for (auto &m : runPhaseMacros) {
|
||||
if (m && m->Matched()) {
|
||||
vblog(LOG_INFO, "running macro: %s", m->Name().c_str());
|
||||
if (!m->PerformActions()) {
|
||||
blog(LOG_WARNING, "abort macro: %s",
|
||||
@@ -753,6 +800,9 @@ bool SwitcherData::runMacros()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (GetLock()) {
|
||||
GetLock()->lock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,11 +55,11 @@ public:
|
||||
std::shared_ptr<Macro> item);
|
||||
bool IsGroup() { return _isGroup; }
|
||||
uint32_t GroupSize() { return _groupSize; }
|
||||
bool IsSubitem() { return !!_parent; }
|
||||
bool IsSubitem() { return !_parent.expired(); }
|
||||
void SetCollapsed(bool val) { _isCollapsed = val; }
|
||||
bool IsCollapsed() { return _isCollapsed; }
|
||||
void SetParent(Macro *m) { _parent = m; }
|
||||
Macro *Parent() { return _parent; }
|
||||
void SetParent(std::shared_ptr<Macro> m) { _parent = m; }
|
||||
Macro *Parent();
|
||||
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
@@ -93,7 +93,7 @@ private:
|
||||
std::deque<std::shared_ptr<MacroCondition>> _conditions;
|
||||
std::deque<std::shared_ptr<MacroAction>> _actions;
|
||||
|
||||
Macro *_parent = nullptr;
|
||||
std::weak_ptr<Macro> _parent;
|
||||
uint32_t _groupSize = 0;
|
||||
|
||||
bool _runInParallel = false;
|
||||
|
||||
@@ -17,5 +17,5 @@ install_advss_plugin_dependency(...)
|
||||
... to install the plugin and its dependencies.
|
||||
#]]
|
||||
|
||||
add_subdirectory(opencv)
|
||||
add_subdirectory(openvr)
|
||||
add_subdirectory(video)
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(advanced-scene-switcher-opencv)
|
||||
|
||||
# --- Check OpenCV requirements ---
|
||||
|
||||
find_package(OpenCV)
|
||||
if(NOT OpenCV_FOUND)
|
||||
message(
|
||||
WARNING
|
||||
"OpenCV not found! Video condition will be disabled!\nOpenCV sources are available under: ${CMAKE_CURRENT_SOURCE_DIR}/../../../deps/opencv"
|
||||
)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# --- End of section ---
|
||||
|
||||
add_library(advanced-scene-switcher-opencv MODULE)
|
||||
target_sources(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE area-selection.cpp
|
||||
area-selection.hpp
|
||||
macro-condition-video.cpp
|
||||
macro-condition-video.hpp
|
||||
opencv-helpers.cpp
|
||||
opencv-helpers.hpp
|
||||
paramerter-wrappers.cpp
|
||||
paramerter-wrappers.hpp
|
||||
preview-dialog.cpp
|
||||
preview-dialog.hpp)
|
||||
|
||||
setup_advss_plugin(${PROJECT_NAME})
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
|
||||
|
||||
# --- OpenCV build settings ---
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE "${OpenCV_INCLUDE_DIRS}")
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ${OpenCV_LIBRARIES})
|
||||
|
||||
# --- End of section ---
|
||||
|
||||
install_advss_plugin(${PROJECT_NAME})
|
||||
if(NOT OS_LINUX)
|
||||
install_advss_plugin_dependency(TARGET ${PROJECT_NAME} DEPENDENCIES
|
||||
${OpenCV_LIBS})
|
||||
endif()
|
||||
@@ -1,113 +0,0 @@
|
||||
#include "opencv-helpers.hpp"
|
||||
|
||||
PatternImageData createPatternData(QImage &pattern)
|
||||
{
|
||||
PatternImageData data{};
|
||||
if (pattern.isNull()) {
|
||||
return data;
|
||||
}
|
||||
|
||||
data.rgbaPattern = QImageToMat(pattern);
|
||||
std::vector<cv::Mat1b> rgbaChannelsPattern;
|
||||
cv::split(data.rgbaPattern, rgbaChannelsPattern);
|
||||
std::vector<cv::Mat1b> rgbChanlesPattern(
|
||||
rgbaChannelsPattern.begin(), rgbaChannelsPattern.begin() + 3);
|
||||
cv::merge(rgbChanlesPattern, data.rgbPattern);
|
||||
cv::threshold(rgbaChannelsPattern[3], data.mask, 0, 255,
|
||||
cv::THRESH_BINARY);
|
||||
return data;
|
||||
}
|
||||
|
||||
void matchPattern(QImage &img, PatternImageData &patternData, double threshold,
|
||||
cv::Mat &result, bool useAlphaAsMask)
|
||||
{
|
||||
if (img.isNull() || patternData.rgbaPattern.empty()) {
|
||||
return;
|
||||
}
|
||||
if (img.height() < patternData.rgbaPattern.rows ||
|
||||
img.width() < patternData.rgbaPattern.cols) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto i = QImageToMat(img);
|
||||
|
||||
if (useAlphaAsMask) {
|
||||
std::vector<cv::Mat1b> rgbaChannelsImage;
|
||||
cv::split(i, rgbaChannelsImage);
|
||||
std::vector<cv::Mat1b> rgbChanlesImage(
|
||||
rgbaChannelsImage.begin(),
|
||||
rgbaChannelsImage.begin() + 3);
|
||||
|
||||
cv::Mat3b rgbImage;
|
||||
cv::merge(rgbChanlesImage, rgbImage);
|
||||
|
||||
cv::matchTemplate(rgbImage, patternData.rgbPattern, result,
|
||||
cv::TM_CCORR_NORMED, patternData.mask);
|
||||
cv::threshold(result, result, threshold, 0, cv::THRESH_TOZERO);
|
||||
|
||||
} else {
|
||||
cv::matchTemplate(i, patternData.rgbaPattern, result,
|
||||
cv::TM_CCOEFF_NORMED);
|
||||
cv::threshold(result, result, threshold, 0, cv::THRESH_TOZERO);
|
||||
}
|
||||
}
|
||||
|
||||
void matchPattern(QImage &img, QImage &pattern, double threshold,
|
||||
cv::Mat &result, bool useAlphaAsMask)
|
||||
{
|
||||
auto data = createPatternData(pattern);
|
||||
matchPattern(img, data, threshold, result, useAlphaAsMask);
|
||||
}
|
||||
|
||||
std::vector<cv::Rect> matchObject(QImage &img, cv::CascadeClassifier &cascade,
|
||||
double scaleFactor, int minNeighbors,
|
||||
cv::Size minSize, cv::Size maxSize)
|
||||
{
|
||||
if (img.isNull() || cascade.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto i = QImageToMat(img);
|
||||
cv::Mat frameGray;
|
||||
cv::cvtColor(i, frameGray, cv::COLOR_BGR2GRAY);
|
||||
equalizeHist(frameGray, frameGray);
|
||||
std::vector<cv::Rect> objects;
|
||||
cascade.detectMultiScale(frameGray, objects, scaleFactor, minNeighbors,
|
||||
0, minSize, maxSize);
|
||||
return objects;
|
||||
}
|
||||
|
||||
uchar getAvgBrightness(QImage &img)
|
||||
{
|
||||
auto i = QImageToMat(img);
|
||||
cv::Mat hsvImage, rgbImage;
|
||||
cv::cvtColor(i, rgbImage, cv::COLOR_RGBA2RGB);
|
||||
cv::cvtColor(rgbImage, hsvImage, cv::COLOR_RGB2HSV);
|
||||
long long brightnessSum = 0;
|
||||
for (int i = 0; i < hsvImage.rows; ++i) {
|
||||
for (int j = 0; j < hsvImage.cols; ++j) {
|
||||
brightnessSum += hsvImage.at<cv::Vec3b>(i, j)[2];
|
||||
}
|
||||
}
|
||||
return brightnessSum / (hsvImage.rows * hsvImage.cols);
|
||||
}
|
||||
|
||||
// Assumption is that QImage uses Format_RGBA8888.
|
||||
// Conversion from: https://github.com/dbzhang800/QtOpenCV
|
||||
cv::Mat QImageToMat(const QImage &img)
|
||||
{
|
||||
if (img.isNull()) {
|
||||
return cv::Mat();
|
||||
}
|
||||
return cv::Mat(img.height(), img.width(), CV_8UC(img.depth() / 8),
|
||||
(uchar *)img.bits(), img.bytesPerLine());
|
||||
}
|
||||
|
||||
QImage MatToQImage(const cv::Mat &mat)
|
||||
{
|
||||
if (mat.empty()) {
|
||||
return QImage();
|
||||
}
|
||||
return QImage(mat.data, mat.cols, mat.rows,
|
||||
QImage::Format::Format_RGBA8888);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
#include <QImage>
|
||||
#undef NO // MacOS macro that can conflict with OpenCV
|
||||
#include <opencv2/opencv.hpp>
|
||||
|
||||
constexpr int minMinNeighbors = 3;
|
||||
constexpr int maxMinNeighbors = 6;
|
||||
constexpr double defaultScaleFactor = 1.1;
|
||||
|
||||
struct PatternImageData {
|
||||
cv::Mat4b rgbaPattern;
|
||||
cv::Mat3b rgbPattern;
|
||||
cv::Mat1b mask;
|
||||
};
|
||||
|
||||
PatternImageData createPatternData(QImage &pattern);
|
||||
void matchPattern(QImage &img, PatternImageData &patternData, double threshold,
|
||||
cv::Mat &result, bool useAlphaAsMask = true);
|
||||
void matchPattern(QImage &img, QImage &pattern, double threshold,
|
||||
cv::Mat &result, bool useAlphaAsMask);
|
||||
std::vector<cv::Rect> matchObject(QImage &img, cv::CascadeClassifier &cascade,
|
||||
double scaleFactor, int minNeighbors,
|
||||
cv::Size minSize, cv::Size maxSize);
|
||||
uchar getAvgBrightness(QImage &img);
|
||||
cv::Mat QImageToMat(const QImage &img);
|
||||
QImage MatToQImage(const cv::Mat &mat);
|
||||
63
src/macro-external/video/CMakeLists.txt
Normal file
63
src/macro-external/video/CMakeLists.txt
Normal file
@@ -0,0 +1,63 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(advanced-scene-switcher-opencv)
|
||||
|
||||
# --- Check OpenCV requirements ---
|
||||
|
||||
find_package(OpenCV)
|
||||
if(NOT OpenCV_FOUND)
|
||||
message(
|
||||
WARNING
|
||||
"OpenCV not found!\n"
|
||||
"Video condition will be disabled!\n\n"
|
||||
"OpenCV sources are available under: ${CMAKE_CURRENT_SOURCE_DIR}/../../../deps/opencv"
|
||||
)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# --- Check optional OCR dependencies ---
|
||||
|
||||
find_package(Leptonica)
|
||||
find_package(Tesseract)
|
||||
|
||||
# --- End of section ---
|
||||
|
||||
add_library(${PROJECT_NAME} MODULE)
|
||||
|
||||
if(Leptonica_FOUND AND Tesseract_FOUND)
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE OCR_SUPPORT)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE Tesseract::libtesseract
|
||||
${Leptonica_LIBRARIES})
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE ${Tesseract_INCLUDE_DIRS}
|
||||
${Leptonica_INCLUDE_DIRS})
|
||||
else()
|
||||
message(
|
||||
WARNING
|
||||
"OCR capabilities of video condition disabled!\n"
|
||||
"Leptonica or Tesseract dependencies were not found!\n\n"
|
||||
"Sources are available under: ${CMAKE_CURRENT_SOURCE_DIR}/../../../deps/")
|
||||
endif()
|
||||
|
||||
target_sources(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE area-selection.cpp
|
||||
area-selection.hpp
|
||||
macro-condition-video.cpp
|
||||
macro-condition-video.hpp
|
||||
opencv-helpers.cpp
|
||||
opencv-helpers.hpp
|
||||
paramerter-wrappers.cpp
|
||||
paramerter-wrappers.hpp
|
||||
preview-dialog.cpp
|
||||
preview-dialog.hpp)
|
||||
|
||||
setup_advss_plugin(${PROJECT_NAME})
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE ${OpenCV_INCLUDE_DIRS})
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ${OpenCV_LIBRARIES})
|
||||
|
||||
install_advss_plugin(${PROJECT_NAME})
|
||||
if(NOT OS_LINUX)
|
||||
install_advss_plugin_dependency(TARGET ${PROJECT_NAME} DEPENDENCIES
|
||||
${OpenCV_LIBS})
|
||||
endif()
|
||||
@@ -61,7 +61,6 @@ SizeSelection::SizeSelection(int min, int max, QWidget *parent)
|
||||
auto layout = new QHBoxLayout();
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addWidget(_x);
|
||||
layout->addWidget(new QLabel(" x "));
|
||||
layout->addWidget(_y);
|
||||
setLayout(layout);
|
||||
}
|
||||
@@ -92,12 +91,14 @@ AreaSelection::AreaSelection(int min, int max, QWidget *parent)
|
||||
_x(new SizeSelection(min, max)),
|
||||
_y(new SizeSelection(min, max))
|
||||
{
|
||||
_x->_x->setToolTip("X");
|
||||
_x->_y->setToolTip("Y");
|
||||
_y->_x->setToolTip(
|
||||
obs_module_text("AdvSceneSwitcher.condition.video.width"));
|
||||
_y->_y->setToolTip(
|
||||
obs_module_text("AdvSceneSwitcher.condition.video.height"));
|
||||
_x->_x->setPrefix("X:");
|
||||
_x->_y->setPrefix("Y:");
|
||||
_y->_x->setPrefix(QString(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.width")) +
|
||||
":");
|
||||
_y->_y->setPrefix(QString(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.height")) +
|
||||
":");
|
||||
|
||||
connect(_x, SIGNAL(SizeChanged(advss::Size)), this,
|
||||
SLOT(XSizeChanged(advss::Size)));
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <QToolTip>
|
||||
#include <QMessageBox>
|
||||
#include <QtGlobal>
|
||||
#include <QColorDialog>
|
||||
|
||||
const std::string MacroConditionVideo::id = "video";
|
||||
|
||||
@@ -35,6 +36,9 @@ const static std::map<VideoCondition, std::string> conditionTypes = {
|
||||
"AdvSceneSwitcher.condition.video.condition.object"},
|
||||
{VideoCondition::BRIGHTNESS,
|
||||
"AdvSceneSwitcher.condition.video.condition.brightness"},
|
||||
#ifdef OCR_SUPPORT
|
||||
{VideoCondition::OCR, "AdvSceneSwitcher.condition.video.condition.ocr"},
|
||||
#endif
|
||||
};
|
||||
|
||||
const static std::map<VideoInput::Type, std::string> videoInputTypes = {
|
||||
@@ -46,6 +50,36 @@ const static std::map<VideoInput::Type, std::string> videoInputTypes = {
|
||||
"AdvSceneSwitcher.condition.video.type.scene"},
|
||||
};
|
||||
|
||||
const static std::map<cv::TemplateMatchModes, std::string> patternMatchModes = {
|
||||
{cv::TemplateMatchModes::TM_CCOEFF_NORMED,
|
||||
"AdvSceneSwitcher.condition.video.patternMatchMode.correlationCoefficient"},
|
||||
{cv::TemplateMatchModes::TM_CCORR_NORMED,
|
||||
"AdvSceneSwitcher.condition.video.patternMatchMode.crossCorrelation"},
|
||||
{cv::TemplateMatchModes::TM_SQDIFF_NORMED,
|
||||
"AdvSceneSwitcher.condition.video.patternMatchMode.squaredDifference"},
|
||||
};
|
||||
|
||||
const static std::map<tesseract::PageSegMode, std::string> pageSegModes = {
|
||||
{tesseract::PageSegMode::PSM_SINGLE_COLUMN,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleColumn"},
|
||||
{tesseract::PageSegMode::PSM_SINGLE_BLOCK_VERT_TEXT,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleBlockVertText"},
|
||||
{tesseract::PageSegMode::PSM_SINGLE_BLOCK,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleBlock"},
|
||||
{tesseract::PageSegMode::PSM_SINGLE_LINE,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleLine"},
|
||||
{tesseract::PageSegMode::PSM_SINGLE_WORD,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleWord"},
|
||||
{tesseract::PageSegMode::PSM_CIRCLE_WORD,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.circleWord"},
|
||||
{tesseract::PageSegMode::PSM_SINGLE_CHAR,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleChar"},
|
||||
{tesseract::PageSegMode::PSM_SPARSE_TEXT,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.sparseText"},
|
||||
{tesseract::PageSegMode::PSM_SPARSE_TEXT_OSD,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.sparseTextOSD"},
|
||||
};
|
||||
|
||||
cv::CascadeClassifier initObjectCascade(std::string &path)
|
||||
{
|
||||
cv::CascadeClassifier cascade;
|
||||
@@ -58,7 +92,7 @@ cv::CascadeClassifier initObjectCascade(std::string &path)
|
||||
return cascade;
|
||||
}
|
||||
|
||||
bool requiresFileInput(VideoCondition t)
|
||||
static bool requiresFileInput(VideoCondition t)
|
||||
{
|
||||
return t == VideoCondition::MATCH || t == VideoCondition::DIFFER ||
|
||||
t == VideoCondition::PATTERN;
|
||||
@@ -126,6 +160,7 @@ bool MacroConditionVideo::Save(obs_data_t *obj) const
|
||||
obs_data_set_double(obj, "brightness", _brightnessThreshold);
|
||||
_patternMatchParameters.Save(obj);
|
||||
_objMatchParameters.Save(obj);
|
||||
_ocrParamters.Save(obj);
|
||||
obs_data_set_bool(obj, "throttleEnabled", _throttleEnabled);
|
||||
obs_data_set_int(obj, "throttleCount", _throttleCount);
|
||||
_areaParameters.Save(obj);
|
||||
@@ -144,6 +179,7 @@ bool MacroConditionVideo::Load(obs_data_t *obj)
|
||||
_brightnessThreshold = obs_data_get_double(obj, "brightness");
|
||||
_patternMatchParameters.Load(obj);
|
||||
_objMatchParameters.Load(obj);
|
||||
_ocrParamters.Load(obj);
|
||||
_throttleEnabled = obs_data_get_bool(obj, "throttleEnabled");
|
||||
_throttleCount = obs_data_get_int(obj, "throttleCount");
|
||||
_areaParameters.Load(obj);
|
||||
@@ -203,12 +239,18 @@ std::string MacroConditionVideo::GetModelDataPath() const
|
||||
return _objMatchParameters.modelPath;
|
||||
}
|
||||
|
||||
void MacroConditionVideo::SetPageSegMode(tesseract::PageSegMode mode)
|
||||
{
|
||||
_ocrParamters.SetPageMode(mode);
|
||||
}
|
||||
|
||||
bool MacroConditionVideo::ScreenshotContainsPattern()
|
||||
{
|
||||
cv::Mat result;
|
||||
matchPattern(_screenshotData.image, _patternImageData,
|
||||
_patternMatchParameters.threshold, result,
|
||||
_patternMatchParameters.useAlphaAsMask);
|
||||
_patternMatchParameters.useAlphaAsMask,
|
||||
_patternMatchParameters.matchMode);
|
||||
return countNonZero(result) > 0;
|
||||
}
|
||||
|
||||
@@ -219,7 +261,8 @@ bool MacroConditionVideo::OutputChanged()
|
||||
_patternImageData = createPatternData(_matchImage);
|
||||
matchPattern(_screenshotData.image, _patternImageData,
|
||||
_patternMatchParameters.threshold, result,
|
||||
_patternMatchParameters.useAlphaAsMask);
|
||||
_patternMatchParameters.useAlphaAsMask,
|
||||
_patternMatchParameters.matchMode);
|
||||
return countNonZero(result) == 0;
|
||||
}
|
||||
return _screenshotData.image != _matchImage;
|
||||
@@ -238,8 +281,31 @@ bool MacroConditionVideo::ScreenshotContainsObject()
|
||||
|
||||
bool MacroConditionVideo::CheckBrightnessThreshold()
|
||||
{
|
||||
_currentBrigthness = getAvgBrightness(_screenshotData.image) / 255.;
|
||||
return _currentBrigthness > _brightnessThreshold;
|
||||
_currentBrightness = getAvgBrightness(_screenshotData.image) / 255.;
|
||||
return _currentBrightness > _brightnessThreshold;
|
||||
}
|
||||
|
||||
bool MacroConditionVideo::CheckOCR()
|
||||
{
|
||||
if (!_ocrParamters.Initialized()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto text = runOCR(_ocrParamters.GetOCR(), _screenshotData.image,
|
||||
_ocrParamters.color);
|
||||
|
||||
if (_ocrParamters.regex.Enabled()) {
|
||||
auto expr = _ocrParamters.regex.GetRegularExpression(
|
||||
_ocrParamters.text);
|
||||
if (!expr.isValid()) {
|
||||
return false;
|
||||
}
|
||||
auto match = expr.match(QString::fromStdString(text));
|
||||
return match.hasMatch();
|
||||
}
|
||||
|
||||
SetVariableValue(text);
|
||||
return text == std::string(_ocrParamters.text);
|
||||
}
|
||||
|
||||
bool MacroConditionVideo::Compare()
|
||||
@@ -251,6 +317,10 @@ bool MacroConditionVideo::Compare()
|
||||
_areaParameters.area.height);
|
||||
}
|
||||
|
||||
if (_condition != VideoCondition::OCR) {
|
||||
SetVariableValue("");
|
||||
}
|
||||
|
||||
switch (_condition) {
|
||||
case VideoCondition::MATCH:
|
||||
return _screenshotData.image == _matchImage;
|
||||
@@ -268,6 +338,8 @@ bool MacroConditionVideo::Compare()
|
||||
return ScreenshotContainsObject();
|
||||
case VideoCondition::BRIGHTNESS:
|
||||
return CheckBrightnessThreshold();
|
||||
case VideoCondition::OCR:
|
||||
return CheckOCR();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -288,6 +360,22 @@ static inline void populateConditionSelection(QComboBox *list)
|
||||
}
|
||||
}
|
||||
|
||||
static inline void populatePageSegModeSelection(QComboBox *list)
|
||||
{
|
||||
for (const auto &[mode, name] : pageSegModes) {
|
||||
list->addItem(obs_module_text(name.c_str()),
|
||||
static_cast<int>(mode));
|
||||
}
|
||||
}
|
||||
|
||||
static inline void populatePatternMatchModeSelection(QComboBox *list)
|
||||
{
|
||||
for (const auto &[mode, name] : patternMatchModes) {
|
||||
list->addItem(obs_module_text(name.c_str()),
|
||||
static_cast<int>(mode));
|
||||
}
|
||||
}
|
||||
|
||||
MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
QWidget *parent, std::shared_ptr<MacroConditionVideo> entryData)
|
||||
: QWidget(parent),
|
||||
@@ -309,6 +397,8 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
"AdvSceneSwitcher.condition.video.patternThresholdDescription"))),
|
||||
_useAlphaAsMask(new QCheckBox(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.patternThresholdUseAlphaAsMask"))),
|
||||
_patternMatchModeLayout(new QHBoxLayout()),
|
||||
_patternMatchMode(new QComboBox()),
|
||||
_brightnessThreshold(new SliderSpinBox(
|
||||
0., 1.,
|
||||
obs_module_text(
|
||||
@@ -316,6 +406,13 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.brightnessThresholdDescription"))),
|
||||
_currentBrightness(new QLabel),
|
||||
_ocrLayout(new QVBoxLayout),
|
||||
_matchText(new VariableTextEdit(this)),
|
||||
_regex(new RegexConfigWidget(this)),
|
||||
_textColor(new QLabel),
|
||||
_selectColor(new QPushButton(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.selectColor"))),
|
||||
_pageSegMode(new QComboBox()),
|
||||
_modelDataPath(new FileSelection()),
|
||||
_modelPathLayout(new QHBoxLayout),
|
||||
_objectScaleThreshold(new SliderSpinBox(
|
||||
@@ -342,15 +439,19 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
_throttleCount(new QSpinBox()),
|
||||
_showMatch(new QPushButton(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.showMatch"))),
|
||||
_previewDialog(this, GetSwitcher()->interval)
|
||||
_previewDialog(this)
|
||||
{
|
||||
_reduceLatency->setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.reduceLatency.tooltip"));
|
||||
_imagePath->Button()->disconnect();
|
||||
_usePatternForChangedCheck->setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.usePatternForChangedCheck.tooltip"));
|
||||
_patternMatchMode->setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.patternMatchMode.tip"));
|
||||
populatePatternMatchModeSelection(_patternMatchMode);
|
||||
_minNeighbors->setMinimum(minMinNeighbors);
|
||||
_minNeighbors->setMaximum(maxMinNeighbors);
|
||||
populatePageSegModeSelection(_pageSegMode);
|
||||
_throttleCount->setMinimum(1 * GetSwitcher()->interval);
|
||||
_throttleCount->setMaximum(10 * GetSwitcher()->interval);
|
||||
_throttleCount->setSingleStep(GetSwitcher()->interval);
|
||||
@@ -380,6 +481,8 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
this, SLOT(PatternThresholdChanged(double)));
|
||||
QWidget::connect(_useAlphaAsMask, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(UseAlphaAsMaskChanged(int)));
|
||||
QWidget::connect(_patternMatchMode, SIGNAL(currentIndexChanged(int)),
|
||||
this, SLOT(PatternMatchModeChanged(int)));
|
||||
QWidget::connect(_brightnessThreshold,
|
||||
SIGNAL(DoubleValueChanged(double)), this,
|
||||
SLOT(BrightnessThresholdChanged(double)));
|
||||
@@ -414,10 +517,26 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
&_previewDialog, SLOT(ConditionChanged(int)));
|
||||
QWidget::connect(_selectArea, SIGNAL(clicked()), this,
|
||||
SLOT(SelectAreaClicked()));
|
||||
QWidget::connect(_selectColor, SIGNAL(clicked()), this,
|
||||
SLOT(SelectColorClicked()));
|
||||
QWidget::connect(_matchText, SIGNAL(textChanged()), this,
|
||||
SLOT(MatchTextChanged()));
|
||||
QWidget::connect(_regex, SIGNAL(RegexConfigChanged(RegexConfig)), this,
|
||||
SLOT(RegexChanged(RegexConfig)));
|
||||
QWidget::connect(_pageSegMode, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(PageSegModeChanged(int)));
|
||||
|
||||
populateVideoInputSelection(_videoInputTypes);
|
||||
populateConditionSelection(_condition);
|
||||
|
||||
_patternMatchModeLayout->setContentsMargins(0, 0, 0, 0);
|
||||
_ocrLayout->setContentsMargins(0, 0, 0, 0);
|
||||
_modelPathLayout->setContentsMargins(0, 0, 0, 0);
|
||||
_neighborsControlLayout->setContentsMargins(0, 0, 0, 0);
|
||||
_sizeLayout->setContentsMargins(0, 0, 0, 0);
|
||||
_checkAreaControlLayout->setContentsMargins(0, 0, 0, 0);
|
||||
_throttleControlLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
QHBoxLayout *entryLine1Layout = new QHBoxLayout;
|
||||
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{videoInputTypes}}", _videoInputTypes},
|
||||
@@ -435,9 +554,17 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
{"{{checkAreaEnable}}", _checkAreaEnable},
|
||||
{"{{checkArea}}", _checkArea},
|
||||
{"{{selectArea}}", _selectArea},
|
||||
{"{{textColor}}", _textColor},
|
||||
{"{{selectColor}}", _selectColor},
|
||||
{"{{textType}}", _pageSegMode},
|
||||
{"{{patternMatchingModes}}", _patternMatchMode},
|
||||
};
|
||||
placeWidgets(obs_module_text("AdvSceneSwitcher.condition.video.entry"),
|
||||
entryLine1Layout, widgetPlaceholders);
|
||||
placeWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.patternMatchMode"),
|
||||
_patternMatchModeLayout, widgetPlaceholders);
|
||||
placeWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.entry.modelPath"),
|
||||
@@ -454,6 +581,24 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
"AdvSceneSwitcher.condition.video.entry.checkArea"),
|
||||
_checkAreaControlLayout, widgetPlaceholders);
|
||||
|
||||
_ocrLayout->addWidget(_matchText);
|
||||
auto regexLayout = new QHBoxLayout;
|
||||
regexLayout->addWidget(_regex);
|
||||
regexLayout->addStretch();
|
||||
_ocrLayout->addLayout(regexLayout);
|
||||
auto pageModeSegLayout = new QHBoxLayout();
|
||||
placeWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.entry.orcTextType"),
|
||||
pageModeSegLayout, widgetPlaceholders);
|
||||
_ocrLayout->addLayout(pageModeSegLayout);
|
||||
auto colorPickLayout = new QHBoxLayout();
|
||||
placeWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.entry.orcColorPick"),
|
||||
colorPickLayout, widgetPlaceholders);
|
||||
_ocrLayout->addLayout(colorPickLayout);
|
||||
|
||||
QGridLayout *sizeGrid = new QGridLayout;
|
||||
sizeGrid->addWidget(
|
||||
new QLabel(obs_module_text(
|
||||
@@ -467,7 +612,6 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
sizeGrid->addWidget(_maxSize, 1, 1);
|
||||
_sizeLayout->addLayout(sizeGrid);
|
||||
_sizeLayout->addStretch();
|
||||
_sizeLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
QHBoxLayout *showMatchLayout = new QHBoxLayout;
|
||||
showMatchLayout->addWidget(_showMatch);
|
||||
@@ -477,8 +621,10 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
mainLayout->addWidget(_usePatternForChangedCheck);
|
||||
mainLayout->addWidget(_patternThreshold);
|
||||
mainLayout->addWidget(_useAlphaAsMask);
|
||||
mainLayout->addLayout(_patternMatchModeLayout);
|
||||
mainLayout->addWidget(_brightnessThreshold);
|
||||
mainLayout->addWidget(_currentBrightness);
|
||||
mainLayout->addLayout(_ocrLayout);
|
||||
mainLayout->addLayout(_modelPathLayout);
|
||||
mainLayout->addWidget(_objectScaleThreshold);
|
||||
mainLayout->addLayout(_neighborsControlLayout);
|
||||
@@ -586,6 +732,8 @@ void MacroConditionVideoEdit::ConditionChanged(int cond)
|
||||
_entryData->_objMatchParameters.cascade =
|
||||
initObjectCascade(path);
|
||||
}
|
||||
|
||||
SetupPreviewDialogParams();
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::ImagePathChanged(const QString &text)
|
||||
@@ -703,8 +851,7 @@ void MacroConditionVideoEdit::UsePatternForChangedCheckChanged(int value)
|
||||
|
||||
std::lock_guard<std::mutex> lock(GetSwitcher()->m);
|
||||
_entryData->_patternMatchParameters.useForChangedCheck = value;
|
||||
_patternThreshold->setVisible(value);
|
||||
adjustSize();
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::PatternThresholdChanged(double value)
|
||||
@@ -742,6 +889,20 @@ void MacroConditionVideoEdit::UseAlphaAsMaskChanged(int value)
|
||||
_entryData->_patternMatchParameters);
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::PatternMatchModeChanged(int idx)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(GetSwitcher()->m);
|
||||
_entryData->_patternMatchParameters.matchMode =
|
||||
static_cast<cv::TemplateMatchModes>(
|
||||
_patternMatchMode->itemData(idx).toInt());
|
||||
_previewDialog.PatternMatchParamtersChanged(
|
||||
_entryData->_patternMatchParameters);
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::BrightnessThresholdChanged(double value)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
@@ -808,6 +969,78 @@ void MacroConditionVideoEdit::MaxSizeChanged(advss::Size value)
|
||||
_entryData->_objMatchParameters);
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::SetupColorLabel(const QColor &color)
|
||||
{
|
||||
_textColor->setText(color.name());
|
||||
_textColor->setPalette(QPalette(color));
|
||||
_textColor->setAutoFillBackground(true);
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::SelectColorClicked()
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QColor color = QColorDialog::getColor(
|
||||
_entryData->_ocrParamters.color, this,
|
||||
obs_module_text("AdvSceneSwitcher.condition.video.selectColor"),
|
||||
QColorDialog::ColorDialogOption());
|
||||
|
||||
if (!color.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SetupColorLabel(color);
|
||||
std::lock_guard<std::mutex> lock(GetSwitcher()->m);
|
||||
_entryData->_ocrParamters.color = color;
|
||||
|
||||
_previewDialog.OCRParamtersChanged(_entryData->_ocrParamters);
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::MatchTextChanged()
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(GetSwitcher()->m);
|
||||
_entryData->_ocrParamters.text =
|
||||
_matchText->toPlainText().toUtf8().constData();
|
||||
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
|
||||
_previewDialog.OCRParamtersChanged(_entryData->_ocrParamters);
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::RegexChanged(RegexConfig conf)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(GetSwitcher()->m);
|
||||
_entryData->_ocrParamters.regex = conf;
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
|
||||
_previewDialog.OCRParamtersChanged(_entryData->_ocrParamters);
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::PageSegModeChanged(int idx)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(GetSwitcher()->m);
|
||||
_entryData->SetPageSegMode(static_cast<tesseract::PageSegMode>(
|
||||
_pageSegMode->itemData(idx).toInt()));
|
||||
|
||||
_previewDialog.OCRParamtersChanged(_entryData->_ocrParamters);
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::CheckAreaEnableChanged(int value)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
@@ -905,37 +1138,37 @@ void MacroConditionVideoEdit::ModelPathChanged(const QString &text)
|
||||
}
|
||||
}
|
||||
|
||||
bool needsShowMatch(VideoCondition cond)
|
||||
static bool needsShowMatch(VideoCondition cond)
|
||||
{
|
||||
return cond == VideoCondition::PATTERN ||
|
||||
cond == VideoCondition::OBJECT;
|
||||
cond == VideoCondition::OBJECT || cond == VideoCondition::OCR;
|
||||
}
|
||||
|
||||
bool needsObjectControls(VideoCondition cond)
|
||||
static bool needsObjectControls(VideoCondition cond)
|
||||
{
|
||||
return cond == VideoCondition::OBJECT;
|
||||
}
|
||||
|
||||
bool needsThrottleControls(VideoCondition cond)
|
||||
static bool needsThrottleControls(VideoCondition cond)
|
||||
{
|
||||
return cond == VideoCondition::PATTERN ||
|
||||
cond == VideoCondition::OBJECT;
|
||||
}
|
||||
|
||||
bool needsThreshold(VideoCondition cond)
|
||||
static bool needsThreshold(VideoCondition cond)
|
||||
{
|
||||
return cond == VideoCondition::PATTERN ||
|
||||
cond == VideoCondition::HAS_CHANGED ||
|
||||
cond == VideoCondition::HAS_NOT_CHANGED;
|
||||
}
|
||||
|
||||
bool patternControlIsOptional(VideoCondition cond)
|
||||
static bool patternControlIsOptional(VideoCondition cond)
|
||||
{
|
||||
return cond == VideoCondition::HAS_CHANGED ||
|
||||
cond == VideoCondition::HAS_NOT_CHANGED;
|
||||
}
|
||||
|
||||
bool needsAreaControls(VideoCondition cond)
|
||||
static bool needsAreaControls(VideoCondition cond)
|
||||
{
|
||||
return cond != VideoCondition::NO_IMAGE;
|
||||
}
|
||||
@@ -951,6 +1184,8 @@ void MacroConditionVideoEdit::SetWidgetVisibility()
|
||||
_patternThreshold->setVisible(needsThreshold(_entryData->_condition));
|
||||
_useAlphaAsMask->setVisible(_entryData->_condition ==
|
||||
VideoCondition::PATTERN);
|
||||
setLayoutVisible(_patternMatchModeLayout,
|
||||
_entryData->_condition == VideoCondition::PATTERN);
|
||||
_brightnessThreshold->setVisible(_entryData->_condition ==
|
||||
VideoCondition::BRIGHTNESS);
|
||||
_currentBrightness->setVisible(_entryData->_condition ==
|
||||
@@ -962,6 +1197,8 @@ void MacroConditionVideoEdit::SetWidgetVisibility()
|
||||
needsObjectControls(_entryData->_condition));
|
||||
_minNeighborsDescription->setVisible(
|
||||
needsObjectControls(_entryData->_condition));
|
||||
setLayoutVisible(_ocrLayout,
|
||||
_entryData->_condition == VideoCondition::OCR);
|
||||
setLayoutVisible(_sizeLayout,
|
||||
needsObjectControls(_entryData->_condition));
|
||||
setLayoutVisible(_modelPathLayout,
|
||||
@@ -977,11 +1214,27 @@ void MacroConditionVideoEdit::SetWidgetVisibility()
|
||||
_entryData->_condition == VideoCondition::HAS_NOT_CHANGED) {
|
||||
_patternThreshold->setVisible(
|
||||
_entryData->_patternMatchParameters.useForChangedCheck);
|
||||
setLayoutVisible(
|
||||
_patternMatchModeLayout,
|
||||
_entryData->_patternMatchParameters.useForChangedCheck);
|
||||
}
|
||||
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::SetupPreviewDialogParams()
|
||||
{
|
||||
_previewDialog.PatternMatchParamtersChanged(
|
||||
_entryData->_patternMatchParameters);
|
||||
_previewDialog.ObjDetectParamtersChanged(
|
||||
_entryData->_objMatchParameters);
|
||||
_previewDialog.OCRParamtersChanged(_entryData->_ocrParamters);
|
||||
_previewDialog.VideoSelectionChanged(_entryData->_video);
|
||||
_previewDialog.AreaParamtersChanged(_entryData->_areaParameters);
|
||||
_previewDialog.ConditionChanged(
|
||||
static_cast<int>(_entryData->_condition));
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::UpdateEntryData()
|
||||
{
|
||||
if (!_entryData) {
|
||||
@@ -1001,10 +1254,17 @@ void MacroConditionVideoEdit::UpdateEntryData()
|
||||
_entryData->_patternMatchParameters.threshold);
|
||||
_useAlphaAsMask->setChecked(
|
||||
_entryData->_patternMatchParameters.useAlphaAsMask);
|
||||
_patternMatchMode->setCurrentIndex(_patternMatchMode->findData(
|
||||
_entryData->_patternMatchParameters.matchMode));
|
||||
_brightnessThreshold->SetDoubleValue(_entryData->_brightnessThreshold);
|
||||
_modelDataPath->SetPath(_entryData->GetModelDataPath().c_str());
|
||||
_objectScaleThreshold->SetDoubleValue(
|
||||
_entryData->_objMatchParameters.scaleFactor);
|
||||
_matchText->setPlainText(_entryData->_ocrParamters.text);
|
||||
_regex->SetRegexConfig(_entryData->_ocrParamters.regex);
|
||||
SetupColorLabel(_entryData->_ocrParamters.color);
|
||||
_pageSegMode->setCurrentIndex(_pageSegMode->findData(
|
||||
static_cast<int>(_entryData->_ocrParamters.GetPageMode())));
|
||||
_minNeighbors->setValue(_entryData->_objMatchParameters.minNeighbors);
|
||||
_minSize->SetSize(_entryData->_objMatchParameters.minSize);
|
||||
_maxSize->SetSize(_entryData->_objMatchParameters.maxSize);
|
||||
@@ -1014,15 +1274,6 @@ void MacroConditionVideoEdit::UpdateEntryData()
|
||||
_checkAreaEnable->setChecked(_entryData->_areaParameters.enable);
|
||||
_checkArea->SetArea(_entryData->_areaParameters.area);
|
||||
UpdatePreviewTooltip();
|
||||
SetupPreviewDialogParams();
|
||||
SetWidgetVisibility();
|
||||
|
||||
// Prepare previewDialog
|
||||
_previewDialog.PatternMatchParamtersChanged(
|
||||
_entryData->_patternMatchParameters);
|
||||
_previewDialog.ObjDetectParamtersChanged(
|
||||
_entryData->_objMatchParameters);
|
||||
_previewDialog.VideoSelectionChanged(_entryData->_video);
|
||||
_previewDialog.AreaParamtersChanged(_entryData->_areaParameters);
|
||||
_previewDialog.ConditionChanged(
|
||||
static_cast<int>(_entryData->_condition));
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <file-selection.hpp>
|
||||
#include <screenshot-helper.hpp>
|
||||
#include <slider-spinbox.hpp>
|
||||
#include <variable-text-edit.hpp>
|
||||
|
||||
#include <QWidget>
|
||||
#include <QComboBox>
|
||||
@@ -21,7 +22,7 @@ class PreviewDialog;
|
||||
|
||||
class MacroConditionVideo : public MacroCondition {
|
||||
public:
|
||||
MacroConditionVideo(Macro *m) : MacroCondition(m) {}
|
||||
MacroConditionVideo(Macro *m) : MacroCondition(m, true){};
|
||||
bool CheckCondition();
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
@@ -37,7 +38,8 @@ public:
|
||||
bool LoadModelData(std::string &path);
|
||||
std::string GetModelDataPath() const;
|
||||
void ResetLastMatch() { _lastMatchResult = false; }
|
||||
double GetCurrentBrightness() const { return _currentBrigthness; }
|
||||
double GetCurrentBrightness() const { return _currentBrightness; }
|
||||
void SetPageSegMode(tesseract::PageSegMode);
|
||||
|
||||
VideoInput _video;
|
||||
VideoCondition _condition = VideoCondition::MATCH;
|
||||
@@ -51,7 +53,8 @@ public:
|
||||
bool _blockUntilScreenshotDone = false;
|
||||
double _brightnessThreshold = 0.5;
|
||||
PatternMatchParameters _patternMatchParameters;
|
||||
ObjDetectParamerts _objMatchParameters;
|
||||
ObjDetectParameters _objMatchParameters;
|
||||
OCRParameters _ocrParamters;
|
||||
AreaParamters _areaParameters;
|
||||
bool _throttleEnabled = false;
|
||||
int _throttleCount = 3;
|
||||
@@ -61,6 +64,7 @@ private:
|
||||
bool ScreenshotContainsPattern();
|
||||
bool ScreenshotContainsObject();
|
||||
bool CheckBrightnessThreshold();
|
||||
bool CheckOCR();
|
||||
bool Compare();
|
||||
bool CheckShouldBeSkipped();
|
||||
|
||||
@@ -71,7 +75,8 @@ private:
|
||||
|
||||
bool _lastMatchResult = false;
|
||||
int _runCount = 0;
|
||||
double _currentBrigthness = 0.;
|
||||
|
||||
double _currentBrightness = 0.;
|
||||
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
@@ -108,6 +113,7 @@ private slots:
|
||||
void UsePatternForChangedCheckChanged(int value);
|
||||
void PatternThresholdChanged(double);
|
||||
void UseAlphaAsMaskChanged(int value);
|
||||
void PatternMatchModeChanged(int value);
|
||||
|
||||
void BrightnessThresholdChanged(double);
|
||||
|
||||
@@ -117,6 +123,11 @@ private slots:
|
||||
void MinSizeChanged(advss::Size value);
|
||||
void MaxSizeChanged(advss::Size value);
|
||||
|
||||
void SelectColorClicked();
|
||||
void MatchTextChanged();
|
||||
void RegexChanged(RegexConfig conf);
|
||||
void PageSegModeChanged(int);
|
||||
|
||||
void CheckAreaEnableChanged(int value);
|
||||
void CheckAreaChanged(advss::Area);
|
||||
void CheckAreaChanged(QRect area);
|
||||
@@ -134,6 +145,8 @@ signals:
|
||||
private:
|
||||
void SetWidgetVisibility();
|
||||
void HandleVideoInputUpdate();
|
||||
void SetupPreviewDialogParams();
|
||||
void SetupColorLabel(const QColor &);
|
||||
|
||||
QComboBox *_videoInputTypes;
|
||||
SceneSelectionWidget *_scenes;
|
||||
@@ -147,10 +160,19 @@ private:
|
||||
|
||||
SliderSpinBox *_patternThreshold;
|
||||
QCheckBox *_useAlphaAsMask;
|
||||
QHBoxLayout *_patternMatchModeLayout;
|
||||
QComboBox *_patternMatchMode;
|
||||
|
||||
SliderSpinBox *_brightnessThreshold;
|
||||
QLabel *_currentBrightness;
|
||||
|
||||
QVBoxLayout *_ocrLayout;
|
||||
VariableTextEdit *_matchText;
|
||||
RegexConfigWidget *_regex;
|
||||
QLabel *_textColor;
|
||||
QPushButton *_selectColor;
|
||||
QComboBox *_pageSegMode;
|
||||
|
||||
FileSelection *_modelDataPath;
|
||||
QHBoxLayout *_modelPathLayout;
|
||||
SliderSpinBox *_objectScaleThreshold;
|
||||
184
src/macro-external/video/opencv-helpers.cpp
Normal file
184
src/macro-external/video/opencv-helpers.cpp
Normal file
@@ -0,0 +1,184 @@
|
||||
#include "opencv-helpers.hpp"
|
||||
|
||||
PatternImageData createPatternData(QImage &pattern)
|
||||
{
|
||||
PatternImageData data{};
|
||||
if (pattern.isNull()) {
|
||||
return data;
|
||||
}
|
||||
|
||||
data.rgbaPattern = QImageToMat(pattern);
|
||||
std::vector<cv::Mat1b> rgbaChannelsPattern;
|
||||
cv::split(data.rgbaPattern, rgbaChannelsPattern);
|
||||
std::vector<cv::Mat1b> rgbChanlesPattern(
|
||||
rgbaChannelsPattern.begin(), rgbaChannelsPattern.begin() + 3);
|
||||
cv::merge(rgbChanlesPattern, data.rgbPattern);
|
||||
cv::threshold(rgbaChannelsPattern[3], data.mask, 0, 255,
|
||||
cv::THRESH_BINARY);
|
||||
return data;
|
||||
}
|
||||
|
||||
static void invertPatternMatchResult(cv::Mat &mat)
|
||||
{
|
||||
for (int r = 0; r < mat.rows; r++) {
|
||||
for (int c = 0; c < mat.cols; c++) {
|
||||
float value = mat.at<float>(r, c) =
|
||||
1.0 - mat.at<float>(r, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void matchPattern(QImage &img, const PatternImageData &patternData,
|
||||
double threshold, cv::Mat &result, bool useAlphaAsMask,
|
||||
cv::TemplateMatchModes matchMode)
|
||||
{
|
||||
if (img.isNull() || patternData.rgbaPattern.empty()) {
|
||||
return;
|
||||
}
|
||||
if (img.height() < patternData.rgbaPattern.rows ||
|
||||
img.width() < patternData.rgbaPattern.cols) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto input = QImageToMat(img);
|
||||
|
||||
if (useAlphaAsMask) {
|
||||
// Remove alpha channel of input image as the alpha channel
|
||||
// information is used as a stencil for the pattern instead and
|
||||
// thus should not be used while matching the pattern as well
|
||||
//
|
||||
// Input format is Format_RGBA8888 so discard the 4th channel
|
||||
std::vector<cv::Mat1b> inputChannels;
|
||||
cv::split(input, inputChannels);
|
||||
std::vector<cv::Mat1b> rgbChanlesImage(
|
||||
inputChannels.begin(), inputChannels.begin() + 3);
|
||||
cv::Mat3b rgbInput;
|
||||
cv::merge(rgbChanlesImage, rgbInput);
|
||||
|
||||
cv::matchTemplate(rgbInput, patternData.rgbPattern, result,
|
||||
matchMode, patternData.mask);
|
||||
} else {
|
||||
cv::matchTemplate(input, patternData.rgbaPattern, result,
|
||||
matchMode);
|
||||
}
|
||||
|
||||
// A perfect match is represented as "0" for TM_SQDIFF_NORMED
|
||||
//
|
||||
// For TM_CCOEFF_NORMED and TM_CCORR_NORMED a perfect match is
|
||||
// represented as "1"
|
||||
if (matchMode == cv::TM_SQDIFF_NORMED) {
|
||||
invertPatternMatchResult(result);
|
||||
}
|
||||
cv::threshold(result, result, threshold, 0.0, cv::THRESH_TOZERO);
|
||||
}
|
||||
|
||||
void matchPattern(QImage &img, QImage &pattern, double threshold,
|
||||
cv::Mat &result, bool useAlphaAsMask,
|
||||
cv::TemplateMatchModes matchColor)
|
||||
{
|
||||
auto data = createPatternData(pattern);
|
||||
matchPattern(img, data, threshold, result, useAlphaAsMask, matchColor);
|
||||
}
|
||||
|
||||
std::vector<cv::Rect> matchObject(QImage &img, cv::CascadeClassifier &cascade,
|
||||
double scaleFactor, int minNeighbors,
|
||||
cv::Size minSize, cv::Size maxSize)
|
||||
{
|
||||
if (img.isNull() || cascade.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto i = QImageToMat(img);
|
||||
cv::Mat frameGray;
|
||||
cv::cvtColor(i, frameGray, cv::COLOR_RGBA2GRAY);
|
||||
equalizeHist(frameGray, frameGray);
|
||||
std::vector<cv::Rect> objects;
|
||||
cascade.detectMultiScale(frameGray, objects, scaleFactor, minNeighbors,
|
||||
0, minSize, maxSize);
|
||||
return objects;
|
||||
}
|
||||
|
||||
uchar getAvgBrightness(QImage &img)
|
||||
{
|
||||
auto i = QImageToMat(img);
|
||||
cv::Mat hsvImage, rgbImage;
|
||||
cv::cvtColor(i, rgbImage, cv::COLOR_RGBA2RGB);
|
||||
cv::cvtColor(rgbImage, hsvImage, cv::COLOR_RGB2HSV);
|
||||
long long brightnessSum = 0;
|
||||
for (int i = 0; i < hsvImage.rows; ++i) {
|
||||
for (int j = 0; j < hsvImage.cols; ++j) {
|
||||
brightnessSum += hsvImage.at<cv::Vec3b>(i, j)[2];
|
||||
}
|
||||
}
|
||||
return brightnessSum / (hsvImage.rows * hsvImage.cols);
|
||||
}
|
||||
|
||||
cv::Mat preprocessForOCR(const QImage &image, const QColor &color)
|
||||
{
|
||||
auto mat = QImageToMat(image);
|
||||
|
||||
// Only keep the desired color
|
||||
cv::cvtColor(mat, mat, cv::COLOR_RGBA2RGB);
|
||||
cv::cvtColor(mat, mat, cv::COLOR_RGB2HSV);
|
||||
cv::inRange(mat, cv::Scalar(0, 0, 0),
|
||||
cv::Scalar(color.red(), color.green(), color.blue()), mat);
|
||||
|
||||
// Invert to improve ORC detection
|
||||
cv::bitwise_not(mat, mat);
|
||||
|
||||
// Scale image up if selected area is too small
|
||||
// Results will probably still be unsatisfying
|
||||
if (mat.rows <= 300 || mat.cols <= 300) {
|
||||
double scale = 0.;
|
||||
if (mat.rows < mat.cols) {
|
||||
scale = 300. / mat.rows;
|
||||
} else {
|
||||
scale = 300. / mat.cols;
|
||||
}
|
||||
|
||||
cv::resize(mat, mat,
|
||||
cv::Size(mat.cols * scale, mat.rows * scale),
|
||||
cv::INTER_CUBIC);
|
||||
}
|
||||
|
||||
return mat;
|
||||
}
|
||||
|
||||
std::string runOCR(tesseract::TessBaseAPI *ocr, const QImage &image,
|
||||
const QColor &color)
|
||||
{
|
||||
#ifdef OCR_SUPPORT
|
||||
auto mat = preprocessForOCR(image, color);
|
||||
ocr->SetImage(mat.data, mat.cols, mat.rows, 1, mat.step);
|
||||
ocr->Recognize(0);
|
||||
std::unique_ptr<char[]> detectedText(ocr->GetUTF8Text());
|
||||
|
||||
if (!detectedText) {
|
||||
return "";
|
||||
}
|
||||
return detectedText.get();
|
||||
|
||||
#else
|
||||
return "";
|
||||
#endif
|
||||
}
|
||||
|
||||
// Assumption is that QImage uses Format_RGBA8888.
|
||||
// Conversion from: https://github.com/dbzhang800/QtOpenCV
|
||||
cv::Mat QImageToMat(const QImage &img)
|
||||
{
|
||||
if (img.isNull()) {
|
||||
return cv::Mat();
|
||||
}
|
||||
return cv::Mat(img.height(), img.width(), CV_8UC(img.depth() / 8),
|
||||
(uchar *)img.bits(), img.bytesPerLine());
|
||||
}
|
||||
|
||||
QImage MatToQImage(const cv::Mat &mat)
|
||||
{
|
||||
if (mat.empty()) {
|
||||
return QImage();
|
||||
}
|
||||
return QImage(mat.data, mat.cols, mat.rows,
|
||||
QImage::Format::Format_RGBA8888);
|
||||
}
|
||||
62
src/macro-external/video/opencv-helpers.hpp
Normal file
62
src/macro-external/video/opencv-helpers.hpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
#include <QImage>
|
||||
#undef NO // MacOS macro that can conflict with OpenCV
|
||||
#include <opencv2/opencv.hpp>
|
||||
|
||||
#ifdef OCR_SUPPORT
|
||||
#include <tesseract/baseapi.h>
|
||||
#else
|
||||
namespace tesseract {
|
||||
enum PageSegMode {
|
||||
PSM_OSD_ONLY = 0,
|
||||
PSM_AUTO_OSD = 1,
|
||||
PSM_AUTO_ONLY = 2,
|
||||
PSM_AUTO = 3,
|
||||
PSM_SINGLE_COLUMN = 4,
|
||||
PSM_SINGLE_BLOCK_VERT_TEXT = 5,
|
||||
PSM_SINGLE_BLOCK = 6,
|
||||
PSM_SINGLE_LINE = 7,
|
||||
PSM_SINGLE_WORD = 8,
|
||||
PSM_CIRCLE_WORD = 9,
|
||||
PSM_SINGLE_CHAR = 10,
|
||||
PSM_SPARSE_TEXT = 11,
|
||||
PSM_SPARSE_TEXT_OSD = 12,
|
||||
PSM_RAW_LINE = 13,
|
||||
|
||||
PSM_COUNT
|
||||
};
|
||||
|
||||
class TessBaseAPI {
|
||||
public:
|
||||
void SetPageSegMode(PageSegMode) {}
|
||||
int Init(const char *, const char *) { return 0; }
|
||||
void End() {}
|
||||
};
|
||||
}
|
||||
#endif
|
||||
|
||||
constexpr int minMinNeighbors = 3;
|
||||
constexpr int maxMinNeighbors = 6;
|
||||
constexpr double defaultScaleFactor = 1.1;
|
||||
|
||||
struct PatternImageData {
|
||||
cv::Mat4b rgbaPattern;
|
||||
cv::Mat3b rgbPattern;
|
||||
cv::Mat1b mask;
|
||||
};
|
||||
|
||||
PatternImageData createPatternData(QImage &pattern);
|
||||
void matchPattern(QImage &img, const PatternImageData &patternData,
|
||||
double threshold, cv::Mat &result, bool useAlphaAsMask,
|
||||
cv::TemplateMatchModes matchMode);
|
||||
void matchPattern(QImage &img, QImage &pattern, double threshold,
|
||||
cv::Mat &result, bool useAlphaAsMask,
|
||||
cv::TemplateMatchModes matchMode);
|
||||
std::vector<cv::Rect> matchObject(QImage &img, cv::CascadeClassifier &cascade,
|
||||
double scaleFactor, int minNeighbors,
|
||||
cv::Size minSize, cv::Size maxSize);
|
||||
uchar getAvgBrightness(QImage &img);
|
||||
cv::Mat preprocessForOCR(const QImage &image, const QColor &color);
|
||||
std::string runOCR(tesseract::TessBaseAPI *, const QImage &, const QColor &);
|
||||
cv::Mat QImageToMat(const QImage &img);
|
||||
QImage MatToQImage(const cv::Mat &mat);
|
||||
@@ -6,6 +6,7 @@ bool PatternMatchParameters::Save(obs_data_t *obj) const
|
||||
obs_data_set_bool(data, "useForChangedCheck", useForChangedCheck);
|
||||
obs_data_set_double(data, "threshold", threshold);
|
||||
obs_data_set_bool(data, "useAlphaAsMask", useAlphaAsMask);
|
||||
obs_data_set_int(data, "matchMode", matchMode);
|
||||
obs_data_set_obj(obj, "patternMatchData", data);
|
||||
obs_data_release(data);
|
||||
return true;
|
||||
@@ -25,11 +26,18 @@ bool PatternMatchParameters::Load(obs_data_t *obj)
|
||||
useForChangedCheck = obs_data_get_bool(data, "useForChangedCheck");
|
||||
threshold = obs_data_get_double(data, "threshold");
|
||||
useAlphaAsMask = obs_data_get_bool(data, "useAlphaAsMask");
|
||||
// TODO: Remove this fallback in a future version
|
||||
if (!obs_data_has_user_value(obj, "matchMode")) {
|
||||
matchMode = cv::TM_CCORR_NORMED;
|
||||
} else {
|
||||
matchMode = static_cast<cv::TemplateMatchModes>(
|
||||
obs_data_get_int(data, "matchMode"));
|
||||
}
|
||||
obs_data_release(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ObjDetectParamerts::Save(obs_data_t *obj) const
|
||||
bool ObjDetectParameters::Save(obs_data_t *obj) const
|
||||
{
|
||||
auto data = obs_data_create();
|
||||
obs_data_set_string(data, "modelPath", modelPath.c_str());
|
||||
@@ -53,7 +61,7 @@ bool isMinNeighborsValid(int minNeighbors)
|
||||
minNeighbors <= maxMinNeighbors;
|
||||
}
|
||||
|
||||
bool ObjDetectParamerts::Load(obs_data_t *obj)
|
||||
bool ObjDetectParameters::Load(obs_data_t *obj)
|
||||
{
|
||||
// TODO: Remove this fallback in a future version
|
||||
if (!obs_data_has_user_value(obj, "patternMatchData")) {
|
||||
@@ -188,3 +196,109 @@ OBSWeakSource VideoInput::GetVideo() const
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void SaveColor(obs_data_t *obj, const char *name, const QColor &color)
|
||||
{
|
||||
auto data = obs_data_create();
|
||||
obs_data_set_int(data, "red", color.red());
|
||||
obs_data_set_int(data, "green", color.green());
|
||||
obs_data_set_int(data, "blue", color.blue());
|
||||
obs_data_set_obj(obj, name, data);
|
||||
obs_data_release(data);
|
||||
}
|
||||
|
||||
OCRParameters::OCRParameters()
|
||||
{
|
||||
Setup();
|
||||
}
|
||||
|
||||
OCRParameters::~OCRParameters()
|
||||
{
|
||||
if (!initDone) {
|
||||
return;
|
||||
}
|
||||
ocr->End();
|
||||
}
|
||||
|
||||
OCRParameters::OCRParameters(const OCRParameters &other)
|
||||
: text(other.text),
|
||||
regex(other.regex),
|
||||
color(other.color),
|
||||
pageSegMode(other.pageSegMode)
|
||||
{
|
||||
Setup();
|
||||
if (initDone) {
|
||||
ocr->SetPageSegMode(pageSegMode);
|
||||
}
|
||||
}
|
||||
|
||||
OCRParameters &OCRParameters::operator=(const OCRParameters &other)
|
||||
{
|
||||
text = other.text;
|
||||
regex = other.regex;
|
||||
color = other.color;
|
||||
pageSegMode = other.pageSegMode;
|
||||
ocr->SetPageSegMode(pageSegMode);
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool OCRParameters::Save(obs_data_t *obj) const
|
||||
{
|
||||
auto data = obs_data_create();
|
||||
text.Save(data, "pattern");
|
||||
regex.Save(data);
|
||||
SaveColor(data, "textColor", color);
|
||||
obs_data_set_int(data, "pageSegMode", static_cast<int>(pageSegMode));
|
||||
obs_data_set_obj(obj, "ocrData", data);
|
||||
obs_data_release(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
static QColor LoadColor(obs_data_t *obj, const char *name)
|
||||
{
|
||||
QColor color = Qt::black;
|
||||
auto data = obs_data_get_obj(obj, name);
|
||||
color.setRed(obs_data_get_int(data, "red"));
|
||||
color.setGreen(obs_data_get_int(data, "green"));
|
||||
color.setBlue(obs_data_get_int(data, "blue"));
|
||||
obs_data_release(data);
|
||||
return color;
|
||||
}
|
||||
|
||||
bool OCRParameters::Load(obs_data_t *obj)
|
||||
{
|
||||
auto data = obs_data_get_obj(obj, "ocrData");
|
||||
text.Load(data, "pattern");
|
||||
regex.Load(data);
|
||||
color = LoadColor(data, "textColor");
|
||||
pageSegMode = static_cast<tesseract::PageSegMode>(
|
||||
obs_data_get_int(data, "pageSegMode"));
|
||||
obs_data_release(data);
|
||||
|
||||
if (initDone) {
|
||||
ocr->SetPageSegMode(pageSegMode);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void OCRParameters::SetPageMode(tesseract::PageSegMode mode)
|
||||
{
|
||||
pageSegMode = mode;
|
||||
ocr->SetPageSegMode(mode);
|
||||
}
|
||||
|
||||
void OCRParameters::Setup()
|
||||
{
|
||||
ocr = std::make_unique<tesseract::TessBaseAPI>();
|
||||
if (!ocr) {
|
||||
initDone = false;
|
||||
return;
|
||||
}
|
||||
std::string dataPath = obs_get_module_data_path(obs_current_module()) +
|
||||
std::string("/res/ocr");
|
||||
if (ocr->Init(dataPath.c_str(), "eng") != 0) {
|
||||
initDone = false;
|
||||
return;
|
||||
}
|
||||
initDone = true;
|
||||
}
|
||||
@@ -4,9 +4,17 @@
|
||||
|
||||
#include <source-selection.hpp>
|
||||
#include <scene-selection.hpp>
|
||||
#include <regex-config.hpp>
|
||||
#include <variable.hpp>
|
||||
#include <obs.hpp>
|
||||
#include <obs-module.h>
|
||||
|
||||
#include <QMetaType>
|
||||
|
||||
#ifdef OCR_SUPPORT
|
||||
#include <tesseract/baseapi.h>
|
||||
#endif
|
||||
|
||||
enum class VideoCondition {
|
||||
MATCH,
|
||||
DIFFER,
|
||||
@@ -16,6 +24,7 @@ enum class VideoCondition {
|
||||
PATTERN,
|
||||
OBJECT,
|
||||
BRIGHTNESS,
|
||||
OCR,
|
||||
};
|
||||
|
||||
class VideoInput {
|
||||
@@ -45,10 +54,11 @@ public:
|
||||
QImage image;
|
||||
bool useForChangedCheck = false;
|
||||
bool useAlphaAsMask = false;
|
||||
cv::TemplateMatchModes matchMode = cv::TM_CCORR_NORMED;
|
||||
double threshold = 0.8;
|
||||
};
|
||||
|
||||
class ObjDetectParamerts {
|
||||
class ObjDetectParameters {
|
||||
public:
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
@@ -64,6 +74,36 @@ public:
|
||||
advss::Size maxSize{0, 0};
|
||||
};
|
||||
|
||||
class OCRParameters {
|
||||
public:
|
||||
OCRParameters();
|
||||
~OCRParameters();
|
||||
OCRParameters(const OCRParameters &other);
|
||||
OCRParameters &operator=(const OCRParameters &);
|
||||
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
|
||||
bool Initialized() const { return initDone; }
|
||||
void SetPageMode(tesseract::PageSegMode);
|
||||
tesseract::PageSegMode GetPageMode() const { return pageSegMode; }
|
||||
tesseract::TessBaseAPI *GetOCR() const { return ocr.get(); }
|
||||
|
||||
VariableResolvingString text =
|
||||
obs_module_text("AdvSceneSwitcher.enterText");
|
||||
RegexConfig regex = RegexConfig::PartialMatchRegexConfig();
|
||||
QColor color = Qt::black;
|
||||
|
||||
private:
|
||||
void Setup();
|
||||
|
||||
tesseract::PageSegMode pageSegMode = tesseract::PSM_SINGLE_BLOCK;
|
||||
std::unique_ptr<tesseract::TessBaseAPI> ocr;
|
||||
bool initDone = false;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(OCRParameters)
|
||||
|
||||
class AreaParamters {
|
||||
public:
|
||||
bool Save(obs_data_t *obj) const;
|
||||
@@ -5,12 +5,11 @@
|
||||
|
||||
#include <screenshot-helper.hpp>
|
||||
|
||||
PreviewDialog::PreviewDialog(QWidget *parent, int delay)
|
||||
PreviewDialog::PreviewDialog(QWidget *parent)
|
||||
: QDialog(parent),
|
||||
_scrollArea(new QScrollArea),
|
||||
_imageLabel(new QLabel(this)),
|
||||
_rubberBand(new QRubberBand(QRubberBand::Rectangle, this)),
|
||||
_delay(delay)
|
||||
_rubberBand(new QRubberBand(QRubberBand::Rectangle, this))
|
||||
{
|
||||
setWindowTitle("Advanced Scene Switcher");
|
||||
setWindowFlags(windowFlags() | Qt::WindowMaximizeButtonHint |
|
||||
@@ -39,22 +38,12 @@ PreviewDialog::PreviewDialog(QWidget *parent, int delay)
|
||||
layout->addWidget(_statusLabel);
|
||||
layout->addWidget(_scrollArea);
|
||||
setLayout(layout);
|
||||
|
||||
// This is a workaround to handle random segfaults triggered when using:
|
||||
// QMetaObject::invokeMethod(this, "RedrawImage", Qt::QueuedConnection,
|
||||
// Q_ARG(QImage, image));
|
||||
// from within CheckForMatchLoop().
|
||||
// Even using BlockingQueuedConnection causes deadlocks
|
||||
_timer.setInterval(300);
|
||||
QWidget::connect(&_timer, &QTimer::timeout, this,
|
||||
&PreviewDialog::ResizeImageLabel);
|
||||
_timer.start();
|
||||
}
|
||||
|
||||
void PreviewDialog::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
_selectingArea = true;
|
||||
if (_type == Type::SHOW_MATCH) {
|
||||
if (_type == PreviewType::SHOW_MATCH) {
|
||||
return;
|
||||
}
|
||||
_origin = event->pos();
|
||||
@@ -64,7 +53,7 @@ void PreviewDialog::mousePressEvent(QMouseEvent *event)
|
||||
|
||||
void PreviewDialog::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (_type == Type::SHOW_MATCH) {
|
||||
if (_type == PreviewType::SHOW_MATCH) {
|
||||
return;
|
||||
}
|
||||
_rubberBand->setGeometry(QRect(_origin, event->pos()).normalized());
|
||||
@@ -72,7 +61,7 @@ void PreviewDialog::mouseMoveEvent(QMouseEvent *event)
|
||||
|
||||
void PreviewDialog::mouseReleaseEvent(QMouseEvent *)
|
||||
{
|
||||
if (_type == Type::SHOW_MATCH) {
|
||||
if (_type == PreviewType::SHOW_MATCH) {
|
||||
return;
|
||||
}
|
||||
auto selectionStart =
|
||||
@@ -100,16 +89,16 @@ PreviewDialog::~PreviewDialog()
|
||||
|
||||
void PreviewDialog::ShowMatch()
|
||||
{
|
||||
Start();
|
||||
_type = PreviewType::SHOW_MATCH;
|
||||
_rubberBand->hide();
|
||||
_type = Type::SHOW_MATCH;
|
||||
Start();
|
||||
}
|
||||
|
||||
void PreviewDialog::SelectArea()
|
||||
{
|
||||
_selectingArea = false;
|
||||
_type = PreviewType::SELECT_AREA;
|
||||
Start();
|
||||
_type = Type::SELECT_AREA;
|
||||
DrawFrame();
|
||||
_statusLabel->setText(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.selectArea.status"));
|
||||
@@ -117,11 +106,8 @@ void PreviewDialog::SelectArea()
|
||||
|
||||
void PreviewDialog::Stop()
|
||||
{
|
||||
_stop = true;
|
||||
_cv.notify_all();
|
||||
if (_thread.joinable()) {
|
||||
_thread.join();
|
||||
}
|
||||
_thread.quit();
|
||||
_thread.wait();
|
||||
}
|
||||
|
||||
void PreviewDialog::closeEvent(QCloseEvent *event)
|
||||
@@ -137,12 +123,18 @@ void PreviewDialog::PatternMatchParamtersChanged(
|
||||
_patternImageData = createPatternData(_patternMatchParams.image);
|
||||
}
|
||||
|
||||
void PreviewDialog::ObjDetectParamtersChanged(const ObjDetectParamerts ¶ms)
|
||||
void PreviewDialog::ObjDetectParamtersChanged(const ObjDetectParameters ¶ms)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_mtx);
|
||||
_objDetectParams = params;
|
||||
}
|
||||
|
||||
void PreviewDialog::OCRParamtersChanged(const OCRParameters ¶ms)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_mtx);
|
||||
_ocrParams = params;
|
||||
}
|
||||
|
||||
void PreviewDialog::VideoSelectionChanged(const VideoInput &video)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_mtx);
|
||||
@@ -164,68 +156,66 @@ void PreviewDialog::ConditionChanged(int cond)
|
||||
_condition = static_cast<VideoCondition>(cond);
|
||||
}
|
||||
|
||||
void PreviewDialog::ResizeImageLabel()
|
||||
void PreviewDialog::UpdateStatus(const QString &status)
|
||||
{
|
||||
_statusLabel->setText(status);
|
||||
}
|
||||
|
||||
void PreviewDialog::UpdateImage(const QPixmap &image)
|
||||
{
|
||||
_imageLabel->setPixmap(image);
|
||||
_imageLabel->adjustSize();
|
||||
if (_type == Type::SELECT_AREA && !_selectingArea) {
|
||||
if (_type == PreviewType::SELECT_AREA && !_selectingArea) {
|
||||
DrawFrame();
|
||||
}
|
||||
emit NeedImage(_video, _type, _patternMatchParams, _patternImageData,
|
||||
_objDetectParams, _ocrParams, _areaParams, _condition);
|
||||
}
|
||||
|
||||
void PreviewDialog::Start()
|
||||
{
|
||||
if (_thread.joinable()) {
|
||||
return;
|
||||
}
|
||||
if (!_video.ValidSelection()) {
|
||||
DisplayMessage(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.screenshotFail"));
|
||||
close();
|
||||
return;
|
||||
}
|
||||
_stop = false;
|
||||
_thread = std::thread(&PreviewDialog::CheckForMatchLoop, this);
|
||||
}
|
||||
|
||||
void PreviewDialog::CheckForMatchLoop()
|
||||
{
|
||||
while (!_stop) {
|
||||
std::unique_lock<std::mutex> lock(_mtx);
|
||||
auto source = obs_weak_source_get_source(_video.GetVideo());
|
||||
ScreenshotHelper screenshot(source);
|
||||
obs_source_release(source);
|
||||
_cv.wait_for(lock, std::chrono::milliseconds(_delay));
|
||||
if (_stop || isHidden()) {
|
||||
return;
|
||||
}
|
||||
if (!screenshot.done || !_video.ValidSelection()) {
|
||||
_statusLabel->setText(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.screenshotFail"));
|
||||
_imageLabel->setPixmap(QPixmap());
|
||||
continue;
|
||||
}
|
||||
if (screenshot.image.width() == 0 ||
|
||||
screenshot.image.height() == 0) {
|
||||
_statusLabel->setText(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.screenshotEmpty"));
|
||||
_imageLabel->setPixmap(QPixmap());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_type == Type::SHOW_MATCH) {
|
||||
if (_areaParams.enable) {
|
||||
screenshot.image = screenshot.image.copy(
|
||||
_areaParams.area.x, _areaParams.area.y,
|
||||
_areaParams.area.width,
|
||||
_areaParams.area.height);
|
||||
}
|
||||
MarkMatch(screenshot.image);
|
||||
}
|
||||
_imageLabel->setPixmap(QPixmap::fromImage(screenshot.image));
|
||||
if (_thread.isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PreviewImage *worker = new PreviewImage();
|
||||
worker->moveToThread(&_thread);
|
||||
connect(&_thread, &QThread::finished, worker, &QObject::deleteLater);
|
||||
connect(worker, &PreviewImage::ImageReady, this,
|
||||
&PreviewDialog::UpdateImage);
|
||||
connect(worker, &PreviewImage::StatusUpdate, this,
|
||||
&PreviewDialog::UpdateStatus);
|
||||
connect(this, &PreviewDialog::NeedImage, worker,
|
||||
&PreviewImage::CreateImage);
|
||||
_thread.start();
|
||||
|
||||
emit NeedImage(_video, _type, _patternMatchParams, _patternImageData,
|
||||
_objDetectParams, _ocrParams, _areaParams, _condition);
|
||||
}
|
||||
|
||||
void markPatterns(cv::Mat &matchResult, QImage &image, cv::Mat &pattern)
|
||||
void PreviewDialog::DrawFrame()
|
||||
{
|
||||
if (!_video.ValidSelection()) {
|
||||
return;
|
||||
}
|
||||
auto imageStart =
|
||||
_imageLabel->mapToGlobal(_imageLabel->rect().topLeft());
|
||||
auto windowStart = mapToGlobal(rect().topLeft());
|
||||
_rubberBand->resize(_areaParams.area.width, _areaParams.area.height);
|
||||
_rubberBand->move(
|
||||
_areaParams.area.x + (imageStart.x() - windowStart.x()),
|
||||
_areaParams.area.y + (imageStart.y() - windowStart.y()));
|
||||
_rubberBand->show();
|
||||
}
|
||||
|
||||
void markPatterns(cv::Mat &matchResult, QImage &image, const cv::Mat &pattern)
|
||||
{
|
||||
auto matchImg = QImageToMat(image);
|
||||
for (int row = 0; row < matchResult.rows - 1; row++) {
|
||||
@@ -251,50 +241,91 @@ void markObjects(QImage &image, std::vector<cv::Rect> &objects)
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewDialog::MarkMatch(QImage &screenshot)
|
||||
void PreviewImage::CreateImage(const VideoInput &video, PreviewType type,
|
||||
const PatternMatchParameters &patternMatchParams,
|
||||
const PatternImageData &patternImageData,
|
||||
ObjDetectParameters objDetectParams,
|
||||
OCRParameters ocrParams,
|
||||
const AreaParamters &areaParams,
|
||||
VideoCondition condition)
|
||||
{
|
||||
if (_condition == VideoCondition::PATTERN) {
|
||||
auto source = obs_weak_source_get_source(video.GetVideo());
|
||||
ScreenshotHelper screenshot(source, true);
|
||||
obs_source_release(source);
|
||||
|
||||
if (!video.ValidSelection() || !screenshot.done) {
|
||||
emit StatusUpdate(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.screenshotFail"));
|
||||
emit ImageReady(QPixmap());
|
||||
return;
|
||||
}
|
||||
|
||||
if (screenshot.image.width() == 0 || screenshot.image.height() == 0) {
|
||||
emit StatusUpdate(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.screenshotEmpty"));
|
||||
emit ImageReady(QPixmap());
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == PreviewType::SHOW_MATCH) {
|
||||
if (areaParams.enable) {
|
||||
screenshot.image = screenshot.image.copy(
|
||||
areaParams.area.x, areaParams.area.y,
|
||||
areaParams.area.width, areaParams.area.height);
|
||||
}
|
||||
// Will emit status label update
|
||||
MarkMatch(screenshot.image, patternMatchParams,
|
||||
patternImageData, objDetectParams, ocrParams,
|
||||
condition);
|
||||
} else {
|
||||
emit StatusUpdate("");
|
||||
}
|
||||
emit ImageReady(QPixmap::fromImage(screenshot.image));
|
||||
}
|
||||
|
||||
void PreviewImage::MarkMatch(QImage &screenshot,
|
||||
const PatternMatchParameters &patternMatchParams,
|
||||
const PatternImageData &patternImageData,
|
||||
ObjDetectParameters &objDetectParams,
|
||||
const OCRParameters &ocrParams,
|
||||
VideoCondition condition)
|
||||
{
|
||||
if (condition == VideoCondition::PATTERN) {
|
||||
cv::Mat result;
|
||||
matchPattern(screenshot, _patternImageData,
|
||||
_patternMatchParams.threshold, result,
|
||||
_patternMatchParams.useAlphaAsMask);
|
||||
matchPattern(screenshot, patternImageData,
|
||||
patternMatchParams.threshold, result,
|
||||
patternMatchParams.useAlphaAsMask,
|
||||
patternMatchParams.matchMode);
|
||||
if (countNonZero(result) == 0) {
|
||||
_statusLabel->setText(obs_module_text(
|
||||
emit StatusUpdate(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.patternMatchFail"));
|
||||
} else {
|
||||
_statusLabel->setText(obs_module_text(
|
||||
emit StatusUpdate(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.patternMatchSuccess"));
|
||||
markPatterns(result, screenshot,
|
||||
_patternImageData.rgbaPattern);
|
||||
patternImageData.rgbaPattern);
|
||||
}
|
||||
} else if (_condition == VideoCondition::OBJECT) {
|
||||
auto objects = matchObject(screenshot, _objDetectParams.cascade,
|
||||
_objDetectParams.scaleFactor,
|
||||
_objDetectParams.minNeighbors,
|
||||
_objDetectParams.minSize.CV(),
|
||||
_objDetectParams.maxSize.CV());
|
||||
} else if (condition == VideoCondition::OBJECT) {
|
||||
auto objects = matchObject(screenshot, objDetectParams.cascade,
|
||||
objDetectParams.scaleFactor,
|
||||
objDetectParams.minNeighbors,
|
||||
objDetectParams.minSize.CV(),
|
||||
objDetectParams.maxSize.CV());
|
||||
if (objects.empty()) {
|
||||
_statusLabel->setText(obs_module_text(
|
||||
emit StatusUpdate(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.objectMatchFail"));
|
||||
} else {
|
||||
_statusLabel->setText(obs_module_text(
|
||||
emit StatusUpdate(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.objectMatchSuccess"));
|
||||
markObjects(screenshot, objects);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (condition == VideoCondition::OCR) {
|
||||
auto text =
|
||||
runOCR(ocrParams.GetOCR(), screenshot, ocrParams.color);
|
||||
QString status(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrMatchSuccess"));
|
||||
emit StatusUpdate(status.arg(QString::fromStdString(text)));
|
||||
|
||||
void PreviewDialog::DrawFrame()
|
||||
{
|
||||
if (!_video.ValidSelection()) {
|
||||
return;
|
||||
// TODO: show preprocessed image
|
||||
}
|
||||
auto imageStart =
|
||||
_imageLabel->mapToGlobal(_imageLabel->rect().topLeft());
|
||||
auto windowStart = mapToGlobal(rect().topLeft());
|
||||
_rubberBand->resize(_areaParams.area.width, _areaParams.area.height);
|
||||
_rubberBand->move(
|
||||
_areaParams.area.x + (imageStart.x() - windowStart.x()),
|
||||
_areaParams.area.y + (imageStart.y() - windowStart.y()));
|
||||
_rubberBand->show();
|
||||
}
|
||||
@@ -4,21 +4,40 @@
|
||||
#include <QDialog>
|
||||
#include <QLabel>
|
||||
#include <QScrollArea>
|
||||
#include <QTimer>
|
||||
|
||||
#include <QThread>
|
||||
#include <QMouseEvent>
|
||||
#include <QRubberBand>
|
||||
#include <QPoint>
|
||||
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
|
||||
enum class PreviewType {
|
||||
SHOW_MATCH,
|
||||
SELECT_AREA,
|
||||
};
|
||||
|
||||
class PreviewImage : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public slots:
|
||||
void CreateImage(const VideoInput &, PreviewType,
|
||||
const PatternMatchParameters &,
|
||||
const PatternImageData &, ObjDetectParameters,
|
||||
OCRParameters, const AreaParamters &, VideoCondition);
|
||||
signals:
|
||||
void ImageReady(const QPixmap &);
|
||||
void StatusUpdate(const QString &);
|
||||
|
||||
private:
|
||||
void MarkMatch(QImage &screenshot, const PatternMatchParameters &,
|
||||
const PatternImageData &, ObjDetectParameters &,
|
||||
const OCRParameters &, VideoCondition);
|
||||
};
|
||||
|
||||
class PreviewDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PreviewDialog(QWidget *parent, int delay);
|
||||
PreviewDialog(QWidget *parent);
|
||||
virtual ~PreviewDialog();
|
||||
void ShowMatch();
|
||||
void SelectArea();
|
||||
@@ -27,19 +46,23 @@ public:
|
||||
|
||||
public slots:
|
||||
void PatternMatchParamtersChanged(const PatternMatchParameters &);
|
||||
void ObjDetectParamtersChanged(const ObjDetectParamerts &);
|
||||
void ObjDetectParamtersChanged(const ObjDetectParameters &);
|
||||
void OCRParamtersChanged(const OCRParameters &);
|
||||
void VideoSelectionChanged(const VideoInput &);
|
||||
void AreaParamtersChanged(const AreaParamters &);
|
||||
void ConditionChanged(int cond);
|
||||
private slots:
|
||||
void ResizeImageLabel();
|
||||
void UpdateImage(const QPixmap &);
|
||||
void UpdateStatus(const QString &);
|
||||
signals:
|
||||
void SelectionAreaChanged(QRect area);
|
||||
void NeedImage(const VideoInput &, PreviewType,
|
||||
const PatternMatchParameters &, const PatternImageData &,
|
||||
ObjDetectParameters, OCRParameters,
|
||||
const AreaParamters &, VideoCondition);
|
||||
|
||||
private:
|
||||
void Start();
|
||||
void CheckForMatchLoop();
|
||||
void MarkMatch(QImage &screenshot);
|
||||
void DrawFrame();
|
||||
|
||||
void mousePressEvent(QMouseEvent *event);
|
||||
@@ -49,28 +72,21 @@ private:
|
||||
VideoInput _video;
|
||||
PatternMatchParameters _patternMatchParams;
|
||||
PatternImageData _patternImageData;
|
||||
ObjDetectParamerts _objDetectParams;
|
||||
ObjDetectParameters _objDetectParams;
|
||||
OCRParameters _ocrParams;
|
||||
AreaParamters _areaParams;
|
||||
|
||||
VideoCondition _condition = VideoCondition::PATTERN;
|
||||
QScrollArea *_scrollArea;
|
||||
QLabel *_statusLabel;
|
||||
QLabel *_imageLabel;
|
||||
QTimer _timer;
|
||||
|
||||
QPoint _origin;
|
||||
QRubberBand *_rubberBand = nullptr;
|
||||
std::atomic_bool _selectingArea = {false};
|
||||
|
||||
std::mutex _mtx;
|
||||
std::condition_variable _cv;
|
||||
std::thread _thread;
|
||||
std::atomic_bool _stop = {true};
|
||||
PreviewType _type = PreviewType::SHOW_MATCH;
|
||||
|
||||
enum class Type {
|
||||
SHOW_MATCH,
|
||||
SELECT_AREA,
|
||||
};
|
||||
Type _type = Type::SHOW_MATCH;
|
||||
int _delay = 300;
|
||||
std::mutex _mtx;
|
||||
QThread _thread;
|
||||
};
|
||||
@@ -90,6 +90,8 @@ struct SwitcherData {
|
||||
|
||||
std::condition_variable cv;
|
||||
std::mutex m;
|
||||
std::unique_lock<std::mutex> *mainLoopLock = nullptr;
|
||||
|
||||
bool transitionActive = false;
|
||||
bool waitForTransition = false;
|
||||
bool stop = false;
|
||||
@@ -251,6 +253,7 @@ struct SwitcherData {
|
||||
void Thread();
|
||||
void Start();
|
||||
void Stop();
|
||||
std::unique_lock<std::mutex> *GetLock() { return mainLoopLock; }
|
||||
|
||||
void setWaitScene();
|
||||
bool sceneChangedDuringWait();
|
||||
|
||||
@@ -47,6 +47,13 @@ RegexConfig::GetRegularExpression(const std::string &expr) const
|
||||
return GetRegularExpression(QString::fromStdString(expr));
|
||||
}
|
||||
|
||||
RegexConfig RegexConfig::PartialMatchRegexConfig()
|
||||
{
|
||||
RegexConfig conf;
|
||||
conf._partialMatch = true;
|
||||
return conf;
|
||||
}
|
||||
|
||||
RegexConfigWidget::RegexConfigWidget(QWidget *parent)
|
||||
: QWidget(parent),
|
||||
_openSettings(new QPushButton()),
|
||||
|
||||
@@ -25,6 +25,8 @@ public:
|
||||
QRegularExpression GetRegularExpression(const QString &) const;
|
||||
QRegularExpression GetRegularExpression(const std::string &) const;
|
||||
|
||||
static RegexConfig PartialMatchRegexConfig();
|
||||
|
||||
private:
|
||||
bool _enable = false;
|
||||
bool _partialMatch = false;
|
||||
|
||||
@@ -39,9 +39,8 @@ ScreenshotHelper::~ScreenshotHelper()
|
||||
gs_stagesurface_destroy(stagesurf);
|
||||
gs_texrender_destroy(texrender);
|
||||
obs_leave_graphics();
|
||||
|
||||
obs_remove_tick_callback(ScreenshotTick, this);
|
||||
}
|
||||
obs_remove_tick_callback(ScreenshotTick, this);
|
||||
if (_saveThread.joinable()) {
|
||||
_saveThread.join();
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public:
|
||||
std::chrono::high_resolution_clock::time_point time;
|
||||
|
||||
private:
|
||||
bool _initDone = false;
|
||||
std::atomic_bool _initDone = false;
|
||||
bool _blocking = false;
|
||||
std::thread _saveThread;
|
||||
bool _saveToFile = false;
|
||||
|
||||
Reference in New Issue
Block a user