diff --git a/.gitmodules b/.gitmodules index 632c38e..498a346 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "plugins/win-dshow/libdshowcapture"] path = plugins/win-dshow/libdshowcapture - url = https://github.com/jp9000/libdshowcapture.git + url = https://github.com/obsproject/libdshowcapture.git [submodule "plugins/mac-syphon/syphon-framework"] path = plugins/mac-syphon/syphon-framework url = https://github.com/palana/Syphon-Framework.git @@ -9,10 +9,10 @@ url = https://github.com/Xaymar/obs-studio_amf-encoder-plugin.git [submodule "plugins/obs-browser"] path = plugins/obs-browser - url = https://github.com/kc5nra/obs-browser.git + url = https://github.com/obsproject/obs-browser.git [submodule "plugins/obs-vst"] path = plugins/obs-vst - url = https://github.com/DDRBoxman/obs-vst.git + url = https://github.com/obsproject/obs-vst.git [submodule "plugins/obs-outputs/ftl-sdk"] path = plugins/obs-outputs/ftl-sdk url = https://github.com/Mixer/ftl-sdk.git diff --git a/.travis.yml b/.travis.yml index 9efe056..9100196 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ env: matrix: include: - os: osx + osx_image: xcode9.4 env: - CMAKE_PREFIX_PATH=/usr/local/opt/qt5/lib/cmake - CEF_BUILD_VERSION=3.3282.1726.gc8368c8 @@ -45,7 +46,7 @@ deploy: region: us-west-2 acl: public_read on: - repo: jp9000/obs-studio + repo: obsproject/obs-studio condition: "$TRAVIS_OS_NAME = osx" all_branches: true @@ -63,6 +64,7 @@ notifications: webhooks: urls: - secure: T5RBY818nO40nr5eC8pdrCfAdQKGkjQdbyYw7mfFrhxWxgt/U5tyKXpX0l9zNGfobS0SnLSqF71OrfW04V97oijXx3q5Y24xV6mSrlLQZOq19+XvGp82LDpkVd4yi2N0kBYpoANB9Pkof4jWT/rKfdQCQttluOLjgr5SM0uWHRg= + - secure: EVI2cu5OnNxVTl4jdVppps7O869gGN1PDcSi8fqq/HJVM5kif8iDe4wCrIKv6yWrK3dSNwRgBAwpcPZglRJnKRh23PdFoCdnTjgzBQlmjUR6BYlunQvoKR9mVX6AdT8zrFDgmtC4aOtGD2paptpqt+Equo25KrLwv+qOHJOTrSQ= on_success: change on_failure: always diff --git a/CI/before-deploy-osx.sh b/CI/before-deploy-osx.sh index f1a87f4..38528d6 100755 --- a/CI/before-deploy-osx.sh +++ b/CI/before-deploy-osx.sh @@ -14,11 +14,6 @@ export FILENAME=$FILE_DATE-$GIT_HASH-$TRAVIS_BRANCH-osx.pkg cd ./build -# Move the CEF plugin out before running build_app so that it doesn't get packaged twice -hr "Moving CEF out to preserve linking" -mv ./rundir/RelWithDebInfo/obs-plugins/CEF.app ./ -mv ./rundir/RelWithDebInfo/obs-plugins/obs-browser.so ./ - # Move obslua hr "Moving OBS LUA" mv ./rundir/RelWithDebInfo/data/obs-scripting/obslua.so ./rundir/RelWithDebInfo/bin/ @@ -35,12 +30,20 @@ if [ -n "${TRAVIS_TAG}" ]; then STABLE=true fi -sudo python ../CI/install/osx/build_app.py --public-key ../CI/install/osx/OBSPublicDSAKey.pem --sparkle-framework ../../sparkle/Sparkle.framework --base-url "https://obsproject.com/osx_update" --stable=$STABLE +sudo python ../CI/install/osx/build_app.py --public-key ../CI/install/osx/OBSPublicDSAKey.pem --sparkle-framework ../../sparkle/Sparkle.framework --stable=$STABLE -# Move the CEF plugin back to where it belongs -hr "Moving CEF back" -mv ./CEF.app ./rundir/RelWithDebInfo/obs-plugins/ -mv ./obs-browser.so ./rundir/RelWithDebInfo/obs-plugins/ +# Copy Chromium embedded framework to app Frameworks directory +hr "Copying Chromium Embedded Framework.framework" +sudo mkdir -p OBS.app/Contents/Frameworks +sudo cp -r ../../cef_binary_${CEF_BUILD_VERSION}_macosx64/Release/Chromium\ Embedded\ Framework.framework OBS.app/Contents/Frameworks/ +sudo install_name_tool -change \ + @rpath/Frameworks/Chromium\ Embedded\ Framework.framework/Chromium\ Embedded\ Framework \ + ../../Frameworks/Chromium\ Embedded\ Framework.framework/Chromium\ Embedded\ Framework \ + OBS.app/Contents/Resources/obs-plugins/obs-browser.so +sudo install_name_tool -change \ + @rpath/Frameworks/Chromium\ Embedded\ Framework.framework/Chromium\ Embedded\ Framework \ + ../../Frameworks/Chromium\ Embedded\ Framework.framework/Chromium\ Embedded\ Framework \ + OBS.app/Contents/Resources/obs-plugins/obs-browser-page # Package app hr "Generating .pkg" diff --git a/CI/before-script-osx.sh b/CI/before-script-osx.sh index 24d1ff5..a0cf54b 100755 --- a/CI/before-script-osx.sh +++ b/CI/before-script-osx.sh @@ -1,15 +1,14 @@ # Make sure ccache is found export PATH=/usr/local/opt/ccache/libexec:$PATH -cd ./plugins/obs-browser -git checkout origin/osx -cd - +git fetch --tags mkdir build cd build cmake -DENABLE_SPARKLE_UPDATER=ON \ --DCMAKE_OSX_DEPLOYMENT_TARGET=10.10 \ +-DCMAKE_OSX_DEPLOYMENT_TARGET=10.11 \ +-DQTDIR=/usr/local/Cellar/qt/5.10.1 \ -DDepsPath=/tmp/obsdeps \ -DVLCPath=$PWD/../../vlc-master \ -DBUILD_BROWSER=ON \ --DCEF_ROOT_DIR=$PWD/../../cef_binary_${CEF_BUILD_VERSION}_macosx64 .. \ No newline at end of file +-DCEF_ROOT_DIR=$PWD/../../cef_binary_${CEF_BUILD_VERSION}_macosx64 .. diff --git a/CI/install-dependencies-osx.sh b/CI/install-dependencies-osx.sh index 4d53296..82f3aac 100755 --- a/CI/install-dependencies-osx.sh +++ b/CI/install-dependencies-osx.sh @@ -1,3 +1,9 @@ +hr() { + echo "───────────────────────────────────────────────────" + echo $1 + echo "───────────────────────────────────────────────────" +} + # Exit if something fails set -e @@ -11,40 +17,46 @@ cd ../ # Install Packages app so we can build a package later # http://s.sudre.free.fr/Software/Packages/about.html -wget --retry-connrefused --waitretry=1 https://s3-us-west-2.amazonaws.com/obs-nightly/Packages.pkg +hr "Downloading Packages app" +wget --quiet --retry-connrefused --waitretry=1 https://s3-us-west-2.amazonaws.com/obs-nightly/Packages.pkg sudo installer -pkg ./Packages.pkg -target / brew update #Base OBS Deps and ccache -brew install qt5 jack speexdsp ccache swig +brew install jack speexdsp ccache swig mbedtls +brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/9a70413d137839de0054571e5f85fd07ee400955/Formula/qt.rb export PATH=/usr/local/opt/ccache/libexec:$PATH ccache -s || echo "CCache is not available." # Fetch and untar prebuilt OBS deps that are compatible with older versions of OSX -wget --retry-connrefused --waitretry=1 https://s3-us-west-2.amazonaws.com/obs-nightly/osx-deps.tar.gz -tar -xf ./osx-deps.tar.gz -C /tmp +hr "Downloading OBS deps" +wget --quiet --retry-connrefused --waitretry=1 https://obs-nightly.s3.amazonaws.com/osx-deps-2018-08-09.tar.gz +tar -xf ./osx-deps-2018-08-09.tar.gz -C /tmp # Fetch vlc codebase -wget --retry-connrefused --waitretry=1 -O vlc-master.zip https://github.com/videolan/vlc/archive/master.zip +hr "Downloading VLC repo" +wget --quiet --retry-connrefused --waitretry=1 -O vlc-master.zip https://github.com/videolan/vlc/archive/master.zip unzip -q ./vlc-master.zip # Get sparkle -wget --retry-connrefused --waitretry=1 -O sparkle.tar.bz2 https://github.com/sparkle-project/Sparkle/releases/download/1.16.0/Sparkle-1.16.0.tar.bz2 +hr "Downloading Sparkle framework" +wget --quiet --retry-connrefused --waitretry=1 -O sparkle.tar.bz2 https://github.com/sparkle-project/Sparkle/releases/download/1.20.0/Sparkle-1.20.0.tar.bz2 mkdir ./sparkle tar -xf ./sparkle.tar.bz2 -C ./sparkle sudo cp -R ./sparkle/Sparkle.framework /Library/Frameworks/Sparkle.framework # CEF Stuff -wget --retry-connrefused --waitretry=1 https://obs-nightly.s3-us-west-2.amazonaws.com/cef_binary_${CEF_BUILD_VERSION}_macosx64.tar.bz2 +hr "Downloading CEF" +wget --quiet --retry-connrefused --waitretry=1 https://obs-nightly.s3-us-west-2.amazonaws.com/cef_binary_${CEF_BUILD_VERSION}_macosx64.tar.bz2 tar -xf ./cef_binary_${CEF_BUILD_VERSION}_macosx64.tar.bz2 cd ./cef_binary_${CEF_BUILD_VERSION}_macosx64 # remove a broken test sed -i '.orig' '/add_subdirectory(tests\/ceftests)/d' ./CMakeLists.txt mkdir build cd ./build -cmake -DCMAKE_CXX_FLAGS="-std=c++11 -stdlib=libc++" -DCMAKE_EXE_LINKER_FLAGS="-std=c++11 -stdlib=libc++" -DCMAKE_OSX_DEPLOYMENT_TARGET=10.9 .. +cmake -DCMAKE_CXX_FLAGS="-std=c++11 -stdlib=libc++" -DCMAKE_EXE_LINKER_FLAGS="-std=c++11 -stdlib=libc++" -DCMAKE_OSX_DEPLOYMENT_TARGET=10.11 .. make -j4 mkdir libcef_dll cd ../../ diff --git a/CI/install/osx/CMakeLists.pkgproj b/CI/install/osx/CMakeLists.pkgproj index eff607a..81f1934 100644 --- a/CI/install/osx/CMakeLists.pkgproj +++ b/CI/install/osx/CMakeLists.pkgproj @@ -86,7 +86,7 @@ GID 80 PATH - ../../../build/plugins/obs-browser/CEF.app + ../../../build/plugins/obs-browser/obs-browser-page PATH_TYPE 3 PERMISSIONS diff --git a/CI/install/osx/build_app.py b/CI/install/osx/build_app.py index aa98437..69e7cf2 100644 --- a/CI/install/osx/build_app.py +++ b/CI/install/osx/build_app.py @@ -45,7 +45,7 @@ parser.add_argument('-d', '--base-dir', dest='dir', default='rundir/RelWithDebIn parser.add_argument('-n', '--build-number', dest='build_number', default='0') parser.add_argument('-k', '--public-key', dest='public_key', default='OBSPublicDSAKey.pem') parser.add_argument('-f', '--sparkle-framework', dest='sparkle', default=None) -parser.add_argument('-b', '--base-url', dest='base_url', default='https://builds.catchexception.org/obs-studio') +parser.add_argument('-b', '--base-url', dest='base_url', default='https://obsproject.com/osx_update') parser.add_argument('-u', '--user', dest='user', default='jp9000') parser.add_argument('-c', '--channel', dest='channel', default='master') add_boolean_argument(parser, 'stable', default=False) @@ -82,6 +82,16 @@ for i in candidate_paths: print("Checking " + i) for root, dirs, files in walk(build_path+"/"+i): for file_ in files: + if ".ini" in file_: + continue + if ".png" in file_: + continue + if ".effect" in file_: + continue + if ".py" in file_: + continue + if ".json" in file_: + continue path = root + "/" + file_ try: out = check_output("{0}otool -L '{1}'".format(args.prefix, path), shell=True, diff --git a/CI/install/osx/post-install.sh b/CI/install/osx/post-install.sh index f55ae58..f1f641a 100644 --- a/CI/install/osx/post-install.sh +++ b/CI/install/osx/post-install.sh @@ -1,5 +1 @@ #!/usr/bin/env bash - -# Fix permissions on CEF -chmod 744 "/Library/Application Support/obs-studio/plugins/obs-browser/bin/CEF.app/Contents/Info.plist" -chmod 744 "/Library/Application Support/obs-studio/plugins/obs-browser/bin/CEF.app/Contents/Frameworks/CEF Helper.app/Contents/Info.plist" diff --git a/CI/util/build-package-deps-osx.sh b/CI/util/build-package-deps-osx.sh index 12fb805..20c0c30 100755 --- a/CI/util/build-package-deps-osx.sh +++ b/CI/util/build-package-deps-osx.sh @@ -1,5 +1,13 @@ #!/usr/bin/env bash +set -e + +# This script builds a tar file that contains a bunch of deps that OBS needs for +# advanced functionality on OSX. Currently this tar file is pulled down off of s3 +# and used in the CI build process on travis. +# Mostly this sets build flags to compile with older SDKS and make sure that +# the libs are portable. + exists() { command -v "$1" >/dev/null 2>&1 @@ -35,15 +43,15 @@ mkdir $DEPS_DEST/include mkdir $DEPS_DEST/lib # OSX COMPAT -export MACOSX_DEPLOYMENT_TARGET=10.9 +export MACOSX_DEPLOYMENT_TARGET=10.11 # If you need an olders SDK and Xcode won't give it to you # https://github.com/phracker/MacOSX-SDKs # libopus -curl -L -O http://downloads.xiph.org/releases/opus/opus-1.1.3.tar.gz -tar -xf opus-1.1.3.tar.gz -cd ./opus-1.1.3 +curl -L -O https://ftp.osuosl.org/pub/xiph/releases/opus/opus-1.2.1.tar.gz +tar -xf opus-1.2.1.tar.gz +cd ./opus-1.2.1 mkdir build cd ./build ../configure --disable-shared --enable-static --prefix="/tmp/obsdeps" @@ -53,9 +61,9 @@ make install cd $WORK_DIR # libogg -curl -L -O http://downloads.xiph.org/releases/ogg/libogg-1.3.2.tar.gz -tar -xf libogg-1.3.2.tar.gz -cd ./libogg-1.3.2 +curl -L -O https://ftp.osuosl.org/pub/xiph/releases/ogg/libogg-1.3.3.tar.gz +tar -xf libogg-1.3.3.tar.gz +cd ./libogg-1.3.3 mkdir build cd ./build ../configure --disable-shared --enable-static --prefix="/tmp/obsdeps" @@ -65,9 +73,9 @@ make install cd $WORK_DIR # libvorbis -curl -L -O http://downloads.xiph.org/releases/vorbis/libvorbis-1.3.5.tar.gz -tar -xf libvorbis-1.3.5.tar.gz -cd ./libvorbis-1.3.5 +curl -L -O https://ftp.osuosl.org/pub/xiph/releases/vorbis/libvorbis-1.3.6.tar.gz +tar -xf libvorbis-1.3.6.tar.gz +cd ./libvorbis-1.3.6 mkdir build cd ./build ../configure --disable-shared --enable-static --prefix="/tmp/obsdeps" @@ -77,12 +85,13 @@ make install cd $WORK_DIR # libvpx -curl -L -O http://storage.googleapis.com/downloads.webmproject.org/releases/webm/libvpx-1.6.0.tar.bz2 -tar -xf libvpx-1.6.0.tar.bz2 -cd ./libvpx-1.6.0 -mkdir build +curl -L -O https://chromium.googlesource.com/webm/libvpx/+archive/v1.7.0.tar.gz +mkdir -p ./libvpx-v1.7.0 +tar -xf v1.7.0.tar.gz -C $PWD/libvpx-v1.7.0 +cd ./libvpx-v1.7.0 +mkdir -p build cd ./build -../configure --disable-shared --libdir="/tmp/obsdeps/bin" +../configure --disable-shared --prefix="/tmp/obsdeps" --libdir="/tmp/obsdeps/lib" make -j 12 make install @@ -94,10 +103,10 @@ cd ./x264 git checkout origin/stable mkdir build cd ./build -../configure --extra-ldflags="-mmacosx-version-min=10.9" --enable-static --prefix="/tmp/obsdeps" +../configure --extra-ldflags="-mmacosx-version-min=10.11" --enable-static --prefix="/tmp/obsdeps" make -j 12 make install -../configure --extra-ldflags="-mmacosx-version-min=10.9" --enable-shared --libdir="/tmp/obsdeps/bin" --prefix="/tmp/obsdeps" +../configure --extra-ldflags="-mmacosx-version-min=10.11" --enable-shared --libdir="/tmp/obsdeps/bin" --prefix="/tmp/obsdeps" make -j 12 ln -f -s libx264.*.dylib libx264.dylib find . -name \*.dylib -exec cp \{\} $DEPS_DEST/bin/ \; @@ -107,9 +116,9 @@ rsync -avh --include="*/" --include="*.h" --exclude="*" ./* $DEPS_DEST/include/ cd $WORK_DIR # janson -curl -L -O http://www.digip.org/jansson/releases/jansson-2.9.tar.gz -tar -xf jansson-2.9.tar.gz -cd jansson-2.9 +curl -L -O http://www.digip.org/jansson/releases/jansson-2.11.tar.gz +tar -xf jansson-2.11.tar.gz +cd jansson-2.11 mkdir build cd ./build ../configure --libdir="/tmp/obsdeps/bin" --enable-shared --disable-static @@ -124,12 +133,12 @@ export LDFLAGS="-L/tmp/obsdeps/lib" export CFLAGS="-I/tmp/obsdeps/include" # FFMPEG -curl -L -O https://github.com/FFmpeg/FFmpeg/archive/n3.2.2.zip -unzip ./n3.2.2.zip -cd ./FFmpeg-n3.2.2 +curl -L -O https://github.com/FFmpeg/FFmpeg/archive/n4.0.2.zip +unzip ./n4.0.2.zip +cd ./FFmpeg-n4.0.2 mkdir build cd ./build -../configure --extra-ldflags="-mmacosx-version-min=10.9" --enable-shared --disable-static --shlibdir="/tmp/obsdeps/bin" --enable-gpl --disable-doc --enable-libx264 --enable-libopus --enable-libvorbis --enable-libvpx --disable-outdev=sdl +../configure --pkg-config-flags="--static" --extra-ldflags="-mmacosx-version-min=10.11" --enable-shared --disable-static --shlibdir="/tmp/obsdeps/bin" --enable-gpl --disable-doc --enable-libx264 --enable-libopus --enable-libvorbis --enable-libvpx --disable-outdev=sdl make -j 12 find . -name \*.dylib -exec cp \{\} $DEPS_DEST/bin/ \; rsync -avh --include="*/" --include="*.h" --exclude="*" ../* $DEPS_DEST/include/ @@ -149,4 +158,4 @@ cd $WORK_DIR tar -czf osx-deps.tar.gz obsdeps -cp ./osx-deps.tar.gz $CURDIR \ No newline at end of file +cp ./osx-deps.tar.gz $CURDIR diff --git a/CMakeLists.txt b/CMakeLists.txt index abe9895..5bac27c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,10 @@ cmake_minimum_required(VERSION 2.8.12) +if (UNIX AND POLICY CMP0072) + # In case of both legacy and glvnd OpenGL libraries found. Prefer GLVND + cmake_policy(SET CMP0072 NEW) +endif() + project(obs-studio) option(BUILD_CAPTIONS "Build captions" FALSE) @@ -27,6 +32,23 @@ include(ObsHelpers) include(ObsCpack) include(GNUInstallDirs) +# Must be a string in the format of "x.x.x-rcx" +if(DEFINED RELEASE_CANDIDATE) + set(OBS_VERSION "${RELEASE_CANDIDATE}") + string(REPLACE "-rc" "." RC_SPLIT ${RELEASE_CANDIDATE}) + string(REPLACE "." ";" RC_SPLIT ${RC_SPLIT}) + message(WARNING "******************************************************************************\nRelease candidate deteced, OBS_VERSION is now: ${OBS_VERSION}\n******************************************************************************") + list(GET RC_SPLIT 0 OBS_RELEASE_CANDIDATE_MAJOR) + list(GET RC_SPLIT 1 OBS_RELEASE_CANDIDATE_MINOR) + list(GET RC_SPLIT 2 OBS_RELEASE_CANDIDATE_PATCH) + list(GET RC_SPLIT 3 OBS_RELEASE_CANDIDATE) +else() + set(OBS_RELEASE_CANDIDATE_MAJOR 0) + set(OBS_RELEASE_CANDIDATE_MINOR 0) + set(OBS_RELEASE_CANDIDATE_PATCH 0) + set(OBS_RELEASE_CANDIDATE 0) +endif() + if(MSVC AND NOT EXISTS "${CMAKE_BINARY_DIR}/ALL_BUILD.vcxproj.user") file(GENERATE OUTPUT "${CMAKE_BINARY_DIR}/ALL_BUILD.vcxproj.user" diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 29518b1..6ea05c2 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -39,8 +39,8 @@ Coding Guidelines - 80 columns max -Commit Guidlines ----------------- +Commit Guidelines +----------------- - OBS Studio uses the 50/72 standard for commits. 50 characters max for the title (excluding module prefix), an empty line, and then a diff --git a/INSTALL b/INSTALL index 258ea26..cf2707d 100644 --- a/INSTALL +++ b/INSTALL @@ -1 +1 @@ -For install instructions please visit https://github.com/jp9000/obs-studio/wiki/Install-Instructions +For install instructions please visit https://github.com/obsproject/obs-studio/wiki/Install-Instructions diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 880869c..f414488 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -55,6 +55,8 @@ include_directories(${FFMPEG_INCLUDE_DIRS}) include_directories(SYSTEM "obs-frontend-api") include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/libobs") include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/deps/libff") +include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/deps/json11") +include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/plugins/obs-browser/panel") find_package(Libcurl REQUIRED) include_directories(${LIBCURL_INCLUDE_DIRS}) @@ -127,6 +129,7 @@ endif() set(obs_SOURCES ${obs_PLATFORM_SOURCES} ${obs_libffutil_SOURCES} + ../deps/json11/json11.cpp obs-app.cpp api-interface.cpp window-basic-main.cpp @@ -152,6 +155,7 @@ set(obs_SOURCES window-log-reply.cpp window-projector.cpp window-remux.cpp + source-tree.cpp properties-view.cpp focus-list.cpp menu-button.cpp @@ -161,6 +165,7 @@ set(obs_SOURCES item-widget-helpers.cpp visibility-checkbox.cpp locked-checkbox.cpp + horizontal-scroll-area.cpp vertical-scroll-area.cpp visibility-item-widget.cpp slider-absoluteset-style.cpp @@ -176,6 +181,7 @@ set(obs_SOURCES set(obs_HEADERS ${obs_PLATFORM_HEADERS} ${obs_libffutil_HEADERS} + ../deps/json11/json11.hpp obs-app.hpp platform.hpp window-main.hpp @@ -197,6 +203,7 @@ set(obs_HEADERS window-log-reply.hpp window-projector.hpp window-remux.hpp + source-tree.hpp properties-view.hpp properties-view.moc.hpp display-helpers.hpp @@ -209,6 +216,8 @@ set(obs_HEADERS item-widget-helpers.hpp visibility-checkbox.hpp locked-checkbox.hpp + horizontal-scroll-area.hpp + expand-checkbox.hpp vertical-scroll-area.hpp visibility-item-widget.hpp slider-absoluteset-style.hpp @@ -227,6 +236,7 @@ set(obs_UI forms/AutoConfigVideoPage.ui forms/AutoConfigStreamPage.ui forms/AutoConfigTestPage.ui + forms/ColorSelect.ui forms/OBSLicenseAgreement.ui forms/OBSLogReply.ui forms/OBSBasic.ui diff --git a/UI/adv-audio-control.cpp b/UI/adv-audio-control.cpp index 531b001..2c602b6 100644 --- a/UI/adv-audio-control.cpp +++ b/UI/adv-audio-control.cpp @@ -13,7 +13,7 @@ #define NSEC_PER_MSEC 1000000 #endif -OBSAdvAudioCtrl::OBSAdvAudioCtrl(QGridLayout *layout, obs_source_t *source_) +OBSAdvAudioCtrl::OBSAdvAudioCtrl(QGridLayout *, obs_source_t *source_) : source(source_) { QHBoxLayout *hlayout; @@ -155,20 +155,7 @@ OBSAdvAudioCtrl::OBSAdvAudioCtrl(QGridLayout *layout, obs_source_t *source_) QWidget::connect(mixer6, SIGNAL(clicked(bool)), this, SLOT(mixer6Changed(bool))); - int lastRow = layout->rowCount(); - - idx = 0; - layout->addWidget(nameLabel, lastRow, idx++); - layout->addWidget(volume, lastRow, idx++); - layout->addWidget(forceMonoContainer, lastRow, idx++); - layout->addWidget(panningContainer, lastRow, idx++); - layout->addWidget(syncOffset, lastRow, idx++); -#if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO - layout->addWidget(monitoringType, lastRow, idx++); -#endif - layout->addWidget(mixerContainer, lastRow, idx++); - layout->layout()->setAlignment(mixerContainer, - Qt::AlignHCenter | Qt::AlignVCenter); + setObjectName(sourceName); } OBSAdvAudioCtrl::~OBSAdvAudioCtrl() @@ -184,6 +171,24 @@ OBSAdvAudioCtrl::~OBSAdvAudioCtrl() mixerContainer->deleteLater(); } +void OBSAdvAudioCtrl::ShowAudioControl(QGridLayout *layout) +{ + int lastRow = layout->rowCount(); + int idx = 0; + + layout->addWidget(nameLabel, lastRow, idx++); + layout->addWidget(volume, lastRow, idx++); + layout->addWidget(forceMonoContainer, lastRow, idx++); + layout->addWidget(panningContainer, lastRow, idx++); + layout->addWidget(syncOffset, lastRow, idx++); +#if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO + layout->addWidget(monitoringType, lastRow, idx++); +#endif + layout->addWidget(mixerContainer, lastRow, idx++); + layout->layout()->setAlignment(mixerContainer, + Qt::AlignHCenter | Qt::AlignVCenter); +} + /* ------------------------------------------------------------------------- */ /* OBS source callbacks */ diff --git a/UI/adv-audio-control.hpp b/UI/adv-audio-control.hpp index 38d7608..1352a4b 100644 --- a/UI/adv-audio-control.hpp +++ b/UI/adv-audio-control.hpp @@ -51,6 +51,7 @@ public: virtual ~OBSAdvAudioCtrl(); inline obs_source_t *GetSource() const {return source;} + void ShowAudioControl(QGridLayout *layout); public slots: void SourceFlagsChanged(uint32_t flags); diff --git a/UI/api-interface.cpp b/UI/api-interface.cpp index a580b72..e7cc836 100644 --- a/UI/api-interface.cpp +++ b/UI/api-interface.cpp @@ -93,9 +93,11 @@ struct OBSStudioAPI : obs_frontend_callbacks { { if (main->IsPreviewProgramMode()) { QMetaObject::invokeMethod(main, "TransitionToScene", + WaitConnection(), Q_ARG(OBSSource, OBSSource(scene))); } else { QMetaObject::invokeMethod(main, "SetCurrentScene", + WaitConnection(), Q_ARG(OBSSource, OBSSource(scene)), Q_ARG(bool, false)); } @@ -167,6 +169,19 @@ struct OBSStudioAPI : obs_frontend_callbacks { } } + bool obs_frontend_add_scene_collection( + const char *name) override + { + bool success = false; + QMetaObject::invokeMethod(main, + "AddSceneCollection", + WaitConnection(), + Q_RETURN_ARG(bool, success), + Q_ARG(bool, true), + Q_ARG(QString, QT_UTF8(name))); + return success; + } + void obs_frontend_get_profiles( std::vector &strings) override { @@ -329,6 +344,16 @@ struct OBSStudioAPI : obs_frontend_callbacks { main->SaveProject(); } + void obs_frontend_defer_save_begin(void) override + { + QMetaObject::invokeMethod(main, "DeferSaveBegin"); + } + + void obs_frontend_defer_save_end(void) override + { + QMetaObject::invokeMethod(main, "DeferSaveEnd"); + } + void obs_frontend_add_save_callback(obs_frontend_save_cb callback, void *private_data) override { diff --git a/UI/data/locale.ini b/UI/data/locale.ini index dd9c6c0..00e4c1f 100644 --- a/UI/data/locale.ini +++ b/UI/data/locale.ini @@ -147,3 +147,9 @@ Name=gjuha shqipe [tl-PH] Name=Wikang Tagalog + +[fa-IR] +Name=فارسی + +[gd-GB] +Name=Gàidhlig diff --git a/UI/data/locale/af-ZA.ini b/UI/data/locale/af-ZA.ini index d44bfd8..ffc182a 100644 --- a/UI/data/locale/af-ZA.ini +++ b/UI/data/locale/af-ZA.ini @@ -88,6 +88,9 @@ Rename="Hernoem" + + + diff --git a/UI/data/locale/ar-SA.ini b/UI/data/locale/ar-SA.ini index e013963..ae2c6f9 100644 --- a/UI/data/locale/ar-SA.ini +++ b/UI/data/locale/ar-SA.ini @@ -122,6 +122,12 @@ Basic.AutoConfig.StreamPage.PerformBandwidthTest="تقدير معدل التدف Basic.AutoConfig.StreamPage.PreferHardwareEncoding="تفضيل استخدام الترميز بواسطة الهاردوير" Basic.AutoConfig.StreamPage.PreferHardwareEncoding.ToolTip="استخدام الترميز بالهاردوير يلغي الحاجة الى معظم موارد المعالج, لكن قد يحتاج الى معدل بث أعلى للحفاظ على نفس مستوى الجودة." Basic.AutoConfig.StreamPage.StreamWarning.Title="تحذير يتعلق بالبث" +Basic.AutoConfig.TestPage="النتيجة النهائية" +Basic.AutoConfig.TestPage.SubTitle.Testing="يقوم البرنامج الآن بتنفيذ مجموعة من الإختبارات لتقدير الإعدادات الأكثر مثالية" +Basic.AutoConfig.TestPage.SubTitle.Complete="انتهى الاختبار" +Basic.AutoConfig.TestPage.TestingBandwidth="إجراء اختبار النطاق الترددي، قد يستغرق هذا بضع دقائق..." +Basic.AutoConfig.TestPage.TestingBandwidth.Connecting="الاتصال ب1:%..." +Basic.AutoConfig.TestPage.TestingBandwidth.ConnectFailed="فشل الاتصال بأي سيرفر، رجاءا تحقق من اتصالك بالانترنت وأعد المحاولة." Basic.Stats="إحصائيات" Basic.Stats.CPUUsage="استخدام المعالج" @@ -473,3 +479,6 @@ Basic.Settings.Advanced.Video.ColorRange.Full="كامل" + + + diff --git a/UI/data/locale/bg-BG.ini b/UI/data/locale/bg-BG.ini index 6a155ed..183e2d4 100644 --- a/UI/data/locale/bg-BG.ini +++ b/UI/data/locale/bg-BG.ini @@ -508,10 +508,6 @@ Basic.Settings.General.SaveProjectors="Запамети прожекторите Basic.Settings.General.SwitchOnDoubleClick="Преминаване към сцена при двойно кликване" Basic.Settings.General.StudioPortraitLayout="Включи портретен / вертикален изглед" Basic.Settings.General.MultiviewLayout="Разположение на Множествения Изглед" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Хоризонтално, Горе" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Водоравно, Долу" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Вертикално, от Ляво" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Вертикално, от Дясно" Basic.Settings.Stream="Стрийм" Basic.Settings.Stream.StreamType="Тип Стрийм" @@ -749,3 +745,6 @@ OutputWarnings.MP4Recording="Предупреждение: Записи запа FinalScene.Title="Изтрий Сцената" FinalScene.Text="Трябва да има поне една сцена във наличност." + + + diff --git a/UI/data/locale/bn-BD.ini b/UI/data/locale/bn-BD.ini index b6d333f..8039b73 100644 --- a/UI/data/locale/bn-BD.ini +++ b/UI/data/locale/bn-BD.ini @@ -561,3 +561,6 @@ OutputWarnings.MultiTrackRecording="সতর্কতা: একাধিক OutputWarnings.MP4Recording="সতর্কতা: রেকর্ডিং MP4 কাছে সংরক্ষিত ফাইল (যেমন: BSODs ফলে, বিদ্যুৎ লোকসান, ইত্যাদি।) চূড়ান্ত করা না হলে নির্বাহ হওয়ার সময়ের অপুনরুদ্ধারযোগ্য করা হবে। আপনি যদি রেকর্ড করতে চান একাধিক অডিও ট্র্যাক MKV এবং remux রেকর্ড করা mp4 ব্যবহার করে এটি সম্পন্ন করার পর বিবেচনা (Remux রেকর্ডিং-> ফাইল)" + + + diff --git a/UI/data/locale/ca-ES.ini b/UI/data/locale/ca-ES.ini index eb8a382..1b58259 100644 --- a/UI/data/locale/ca-ES.ini +++ b/UI/data/locale/ca-ES.ini @@ -78,6 +78,8 @@ None="Cap" StudioMode.Preview="Vista prèvia" StudioMode.Program="Programa" ShowInMultiview="Mostra en vista múltiple" +VerticalLayout="Disposició vertical" +Group="Grup" AlreadyRunning.Title="L'OBS ja s'està executant" AlreadyRunning.Text="L'OBS ja s'està executant! A no ser que vulgueu fer això, tanqueu totes les finestres de l'OBS abans d'intentar iniciar una nova. Si teniu configurat OBS perquè es minimitzi a la barra de tasques, proveu a veure si segueix executant-se aquí." @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="S'està aturant la reproducció de la memòria Basic.Main.StopStreaming="Atura l'enregistrament" Basic.Main.StoppingStreaming="Aturant la transmissió..." Basic.Main.ForceStopStreaming="Atura l'enregistrament (descarta el retard)" +Basic.Main.Group="Grup %1" +Basic.Main.GroupItems="Agrupa els elements seleccionats" +Basic.Main.Ungroup="Desagrupa" Basic.MainMenu.File="&Fitxer" Basic.MainMenu.File.Export="&Exporta" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="&Eines" Basic.MainMenu.Help="&Ajuda" Basic.MainMenu.Help.HelpPortal="Portal d'ajuda" Basic.MainMenu.Help.Website="Visa el lloc &web" +Basic.MainMenu.Help.Discord="Uniu-vos a un servidor &Discord" Basic.MainMenu.Help.Logs="Fitxers de ®istre" Basic.MainMenu.Help.Logs.ShowLogs="&Mostra els arxius de registre" Basic.MainMenu.Help.Logs.UploadCurrentLog="Carregar arxiu de registre actual" Basic.MainMenu.Help.Logs.UploadLastLog="Carregar darrer fitxer de registre" Basic.MainMenu.Help.Logs.ViewCurrentLog="&Visualitza el registre actual" Basic.MainMenu.Help.CheckForUpdates="Comprova si hi ha cap actualització" +Basic.MainMenu.Help.CrashLogs="Info&rme de fallada" +Basic.MainMenu.Help.CrashLogs.ShowLogs="Mo&stra els informes de fallada" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="Penja el darrer informe de fa&llada" Basic.Settings.ProgramRestart="El programa ha de ser re-iniciat per tal que aquesta configuració tingui efecte." Basic.Settings.ConfirmTitle="Confirma els canvis" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Minimitza sempre a la safata del Basic.Settings.General.SaveProjectors="Desa els projectors en sortir" Basic.Settings.General.SwitchOnDoubleClick="Transició a l'escena en fer doble clic" Basic.Settings.General.StudioPortraitLayout="Habilita la disposició horitzontal/vertical" +Basic.Settings.General.Multiview="Vista múltiple" +Basic.Settings.General.Multiview.MouseSwitch="Feu clic per canviar entre escenes" +Basic.Settings.General.Multiview.DrawSourceNames="Mostra el nom de l'escena" +Basic.Settings.General.Multiview.DrawSafeAreas="Dibuixa les zones segures (EBU R 95)" Basic.Settings.General.MultiviewLayout="Disposició de vista múltiple" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horitzontal, amunt" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horitzontal, abaix" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertical, esquerra" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertical, dreta" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horitzontal, part superior (8 escenes)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horitzontal, part inferior (8 escenes)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertical, esquerra (8 escenes)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertical, dreta (8 escenes)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Horitzontal, part superior (24 escenes)" Basic.Settings.Stream="Directe" Basic.Settings.Stream.StreamType="Tipus de directe" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="Resposta del ràtio de l'audiòmetre" Basic.Settings.Audio.MeterDecayRate.Fast="Ràpida" Basic.Settings.Audio.MeterDecayRate.Medium="Mitja (PPM de tipus I)" Basic.Settings.Audio.MeterDecayRate.Slow="Lenta (PPM de tipus II)" +Basic.Settings.Audio.PeakMeterType="Tipus de mesurador de pics" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Pic de mostra" +Basic.Settings.Audio.PeakMeterType.TruePeak="True Peak (ús elevat de la CPU)" Basic.Settings.Audio.MultiChannelWarning.Enabled="ATENCIÓ: El so envoltant està habilitat." Basic.Settings.Audio.MultichannelWarning="Si esteu retransmetent, verifiqueu que el servei triat suporta el so envoltant, tant a la reproducció d'entrada com de sortida. El Twitch, el Facebook 360 Live, el Mixer RTMP i el Smashcast són exemples on està completament suportat. Encara que el Facebook Live i el YouTube Live accepten l'entrada de so envoltant, el Facebook Live la converteix a estèreo i el YouTube Live la reprodueix només en 2 canals.\n\nEls filtres d'àudio de l'OBS són compatibles amb el so envoltant, encara que no es garanteix la compatibilitat amb connectors VST." Basic.Settings.Audio.MultichannelWarning.Title="Voleu habilitar el so envoltant?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="Xarxa" Basic.Settings.Advanced.Network.BindToIP="Enllaçar amb" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Activa el nou codi de xarxa" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Mode de baixa latència" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Inhabilita les tecles de drecera quan la finestra principal estigui en primer pla" Basic.AdvAudio="&Propietats avançades d'àudio" Basic.AdvAudio.Name="Nom" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="Advertència: Els enregistraments desats en MP4 ser FinalScene.Title="Supressió de l'escena" FinalScene.Text="Cal que hi hagi almenys una escena." +NoSources.Title="Cap font" +NoSources.Text="Sembla que encara no heu afegit cap font de vídeo, de manera que només es mostrarà una pantalla en blanc. Esteu segur que voleu fer això?" +NoSources.Text.AddSource="Podeu afegir fonts fent clic a la icona «+» sota el quadre Fonts de la finestra principal en qualsevol moment." + +ChangeBG="Estableix el color" +CustomColor="Color personalitzat" + +BrowserSource.EnableHardwareAcceleration="Habilita l'acceleració per maquinari al navegador" + diff --git a/UI/data/locale/cs-CZ.ini b/UI/data/locale/cs-CZ.ini index 4b2bc0a..8ae5fd3 100644 --- a/UI/data/locale/cs-CZ.ini +++ b/UI/data/locale/cs-CZ.ini @@ -78,6 +78,8 @@ None="Žádný" StudioMode.Preview="Náhled" StudioMode.Program="Program" ShowInMultiview="Zobrazit v Multiview" +VerticalLayout="Vertikální rozložení" +Group="Skupina" AlreadyRunning.Title="OBS je již spuštěno" AlreadyRunning.Text="OBS již běží! Pokud jste to opravdu nechtěli udělat, tak prosím ukončete ostatní běžící instance programu OBS před spuštěním nové. Pokud máte nastavenu minimalizaci do lišty, tak se prosím podívejte, zda neběží tam." @@ -404,7 +406,10 @@ Basic.Main.StopReplayBuffer="Zastavit záznam do paměti" Basic.Main.StoppingReplayBuffer="Zastavuji záznam do paměti..." Basic.Main.StopStreaming="Zastavit vysílání" Basic.Main.StoppingStreaming="Zastavuji vysílání..." -Basic.Main.ForceStopStreaming="Zastavit vysání (bez zpoždění)" +Basic.Main.ForceStopStreaming="Zastavit vysílání (bez zpoždění)" +Basic.Main.Group="Skupina %1" +Basic.Main.GroupItems="Seskupit vybrané" +Basic.Main.Ungroup="Rozdělit skupinu" Basic.MainMenu.File="Soubor (&F)" Basic.MainMenu.File.Export="&Exportovat" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="Nás&troje" Basic.MainMenu.Help="Pomoc (&H)" Basic.MainMenu.Help.HelpPortal="&Portál pomoci" Basic.MainMenu.Help.Website="Navštívit &web" +Basic.MainMenu.Help.Discord="Připojit se na &Discord server" Basic.MainMenu.Help.Logs="Soubory záznamu (&L)" Basic.MainMenu.Help.Logs.ShowLogs="Zobrazit soubory záznamu (&S)" Basic.MainMenu.Help.Logs.UploadCurrentLog="Nahrát aktuální soubor záznamu (&C)" Basic.MainMenu.Help.Logs.UploadLastLog="Nahrát poslední soubor záznamu (&L)" Basic.MainMenu.Help.Logs.ViewCurrentLog="Zobrazit aktuální záznam (&V)" Basic.MainMenu.Help.CheckForUpdates="Zkontrolovat aktualizace" +Basic.MainMenu.Help.CrashLogs="Hlášení o pádech (&R)" +Basic.MainMenu.Help.CrashLogs.ShowLogs="Zobrazit hlášení o pádech (&S)" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="Nahrát pos&lední hlášení" Basic.Settings.ProgramRestart="Pro projevení nastavení je potřeba restartovat aplikaci." Basic.Settings.ConfirmTitle="Potvrzení změn" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Vždy minimalizovat do systémov Basic.Settings.General.SaveProjectors="Ukládat projektory při ukončení" Basic.Settings.General.SwitchOnDoubleClick="Přejít na scénu po dvojitém kliknutí" Basic.Settings.General.StudioPortraitLayout="Povolit rozložení na výšku (portrét)" +Basic.Settings.General.Multiview="Multiview" +Basic.Settings.General.Multiview.MouseSwitch="Kliknutí pro přechod mezi scénami" +Basic.Settings.General.Multiview.DrawSourceNames="Zobrazovat názvy scén" +Basic.Settings.General.Multiview.DrawSafeAreas="Vyznačovat viditelné oblasti (EBU R 95)" Basic.Settings.General.MultiviewLayout="Rozložení Multiview" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horizontálně, nahoře" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horizontálně, dole" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertikálně, vlevo" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertikálně, vpravo" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horizontálně, nahoře (8 scén)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horizontálně, dole (8 scén)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertikálně, vlevo (8 scén)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertikálně, vpravo (8 scén)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Horizontálně, nahoře (24 scén)" Basic.Settings.Stream="Vysílání" Basic.Settings.Stream.StreamType="Typ vysílání" @@ -631,9 +645,13 @@ Basic.Settings.Video.DownscaleFilter.Lanczos="Lanczos (Ostré při škálování Basic.Settings.Audio="Zvuk" Basic.Settings.Audio.SampleRate="Vzorkovací frekvence" Basic.Settings.Audio.Channels="Kanály" +Basic.Settings.Audio.MeterDecayRate="Rychlost útlumu snímače zvuku" Basic.Settings.Audio.MeterDecayRate.Fast="Rychle" Basic.Settings.Audio.MeterDecayRate.Medium="Střední (typ I PPM)" Basic.Settings.Audio.MeterDecayRate.Slow="Pomalu (typ II PPM)" +Basic.Settings.Audio.PeakMeterType="Typ měřiče špičky" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Špička vzorky" +Basic.Settings.Audio.PeakMeterType.TruePeak="Pravá špička (Vyšší využití CPU)" Basic.Settings.Audio.MultiChannelWarning.Enabled="VAROVÁNÍ: Prostorový zvuk je zapnut." Basic.Settings.Audio.MultichannelWarning="Předtím než začnete vysílat si zkontrolujte, zda vaše vysílací služba podporuje příjem a přehrávání prostorového zvuku. Twitch, Facebook 360 Live, Mixer RTMP, Smashcast jsou příklady služeb, které jej plně podporují. I když Facebook Live a YouTube Live oba podporují příjem prostorového zvuku, Facebook Live jej převede na stereo a YouTube Live přehrává pouze dva kanály.\n\nOBS filtry zvuku jej plně podporují, ale podpora u pluginu VST není garantována." Basic.Settings.Audio.MultichannelWarning.Title="Povolit prostorový zvuk?" @@ -674,6 +692,7 @@ Basic.Settings.Advanced.Network="Síť" Basic.Settings.Advanced.Network.BindToIP="Svázat s adresou" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Použít nový síťový kód" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Režim nízké odezvy" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Zakázat klávesové zkratky, když je hlavní okno aktivní" Basic.AdvAudio="Rozšířené vlastnosti zvuku" Basic.AdvAudio.Name="Název" @@ -748,3 +767,12 @@ OutputWarnings.MP4Recording="Varování: Nahrávky uložené v MP4 nebude možn FinalScene.Title="Odstranění scény" FinalScene.Text="Musí existovat alespoň jedna scéna, proto tuto není možno odstranit." +NoSources.Title="Žádné zdroje" +NoSources.Text="Vypadá to, že jste zatím nepřidali žádné zdroje obrazu, takže budete vysílat černou obrazovku. Opravdu chcete pokračovat ?" +NoSources.Text.AddSource="Zdroje můžete kdykoliv přidat tlačítkem + pod seznamem Zdroje v hlavním okně." + +ChangeBG="Nastavit barvu" +CustomColor="Vlastní barva" + +BrowserSource.EnableHardwareAcceleration="Zapnout hardwarovou akceleraci pro zdroj prohlížeče" + diff --git a/UI/data/locale/da-DK.ini b/UI/data/locale/da-DK.ini index 15bbbe5..615dcd9 100644 --- a/UI/data/locale/da-DK.ini +++ b/UI/data/locale/da-DK.ini @@ -78,6 +78,8 @@ None="Ingen" StudioMode.Preview="Forhåndsvisning" StudioMode.Program="Program" ShowInMultiview="Vis i Multi View" +VerticalLayout="Lodret layout" +Group="Gruppér" AlreadyRunning.Title="OBS kører allerede" AlreadyRunning.Text="OBS kører allerede! Medmindre dette er tilsigtet, så bedes du lukke enhver eksisterende OBS-proces, inden du forsøger at køre en ny. Hvis du har OBS indstillet til at minimeres sig til systembakken, så tjek venligst om den stadig kører dér." @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="Stopper Genafspilningsbuffer..." Basic.Main.StopStreaming="Stop streaming" Basic.Main.StoppingStreaming="Stopper stream..." Basic.Main.ForceStopStreaming="Stop streaming (ignorer forsinkelse)" +Basic.Main.Group="Gruppe %1" +Basic.Main.GroupItems="Gruppér valgte elementer" +Basic.Main.Ungroup="Ikke-grupperet" Basic.MainMenu.File="&Fil" Basic.MainMenu.File.Export="&Eksport" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="Værk&tøjer" Basic.MainMenu.Help="&Hjælp" Basic.MainMenu.Help.HelpPortal="Hjælp og &Portal" Basic.MainMenu.Help.Website="Besøg &websted" +Basic.MainMenu.Help.Discord="Join &Discord serveren" Basic.MainMenu.Help.Logs="&Logfiler" Basic.MainMenu.Help.Logs.ShowLogs="Vis log-filer (&S)" Basic.MainMenu.Help.Logs.UploadCurrentLog="Upload Aktuelle logfil (&C)" Basic.MainMenu.Help.Logs.UploadLastLog="Upload Sidste logfil (&L)" Basic.MainMenu.Help.Logs.ViewCurrentLog="&Vis aktuel logfil" Basic.MainMenu.Help.CheckForUpdates="Tjek for opdateringer" +Basic.MainMenu.Help.CrashLogs="Nedbrudsrapporter" +Basic.MainMenu.Help.CrashLogs.ShowLogs="&Vis nedbrudsrapporter" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="Up&load seneste nedbrudsrapport" Basic.Settings.ProgramRestart="Programmet skal genstartes, før disse indstillinger træder i kraft." Basic.Settings.ConfirmTitle="Bekræfte ændringer" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Minimer altid til processlinjen i Basic.Settings.General.SaveProjectors="Gem projektorer ved afslutning" Basic.Settings.General.SwitchOnDoubleClick="Overgang til scenen ved dobbeltklik" Basic.Settings.General.StudioPortraitLayout="Aktivere stående/liggende layout" +Basic.Settings.General.Multiview="Multiview" +Basic.Settings.General.Multiview.MouseSwitch="Klik for at skifte mellem scener" +Basic.Settings.General.Multiview.DrawSourceNames="Vis scenenavne" +Basic.Settings.General.Multiview.DrawSafeAreas="Tegn sikre områder (EBU R 95)" Basic.Settings.General.MultiviewLayout="Multivisningslayout" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horisontal, Top" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horisontal, Bund" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertikal, Venstre" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertikal, Højre" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Vandret, øverst (8 scener)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Vandret, nederst (8 scener)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Lodret, venstre (8 scener)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Lodret, højre (8 scener)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Vandret, øverst (24 scener)" Basic.Settings.Stream="Stream" Basic.Settings.Stream.StreamType="Streamtype" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="Lydmålerudstyring, faldhastighed" Basic.Settings.Audio.MeterDecayRate.Fast="Hurtig" Basic.Settings.Audio.MeterDecayRate.Medium="Medium (Type I PPM)" Basic.Settings.Audio.MeterDecayRate.Slow="Langsom (Type II PPM)" +Basic.Settings.Audio.PeakMeterType="Peak Meter-type" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Intervalspidsværdi" +Basic.Settings.Audio.PeakMeterType.TruePeak="Sand spidsværdi (højere CPU-belastning)" Basic.Settings.Audio.MultiChannelWarning.Enabled="ADVARSEL: Surround Sound-lyd er aktiveret." Basic.Settings.Audio.MultichannelWarning="Tjek ifm. streamer, om din streaming-tjeneste understøtter både Surround Sound-input og -afspilning. Twitch, Facebook 360 Live, Mixer RTMP og Smashcast er eksempler, hvor surroundlyd understøttes fuldt ud. Selvom Facebook Live og YouTube Live begge accepterer surround input, nedmikser Facebook Live til stereo, og YouTube Live afspiller kun to kanaler.\n\nOBS-lydfiltre er kompatible med surroundlyd, dog er VST-pluginsupport ikke garanteret." Basic.Settings.Audio.MultichannelWarning.Title="Aktivér Surround Sound-lyd?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="Netværk" Basic.Settings.Advanced.Network.BindToIP="Bind til IP" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Aktivér ny netværkskode" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Lav forsinkelsestilstand" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Deaktivér genvejstaster, når hovedvinduet er i fokus" Basic.AdvAudio="Avancerede lydegenskaber" Basic.AdvAudio.Name="Navn" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="Advarsel: MP4-optagelser vil ikke kunne genoprettes FinalScene.Title="Slet scene" FinalScene.Text="Der kræves mindst én scene." +NoSources.Title="Ingen kilder" +NoSources.Text="Det ser ud til, at du ikke har tilføjet nogen videokilder endnu, så dit output bliver en blank/tom skærm. Sikker på, at du vil gøre dette?" +NoSources.Text.AddSource="Du kan til enhver tid tilføje kilder ved at klikke på +-ikonet under feltet Kilder i hovedvinduet." + +ChangeBG="Sæt farve" +CustomColor="Brugerdefineret farve" + +BrowserSource.EnableHardwareAcceleration="Aktivér Browser Source-hardwareacceleration" + diff --git a/UI/data/locale/de-DE.ini b/UI/data/locale/de-DE.ini index d0f64dc..5051ea5 100644 --- a/UI/data/locale/de-DE.ini +++ b/UI/data/locale/de-DE.ini @@ -78,6 +78,8 @@ None="Keine" StudioMode.Preview="Vorschau" StudioMode.Program="Programm" ShowInMultiview="In Multiview anzeigen" +VerticalLayout="Vertikales Layout" +Group="Gruppe" AlreadyRunning.Title="OBS wird bereits ausgeführt" AlreadyRunning.Text="OBS wird bereits ausgeführt! Bitte beenden Sie alle vorhandenen OBS Instanzen bevor Sie eine neue Instanz starten, es sei denn, Sie tun dies absichtlich. Wenn Sie OBS so eingestellt haben das es sich zum Infobereich minimiert, überprüfen Sie bitte, ob es dort läuft." @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="Stoppe Replaypuffer..." Basic.Main.StopStreaming="Streaming stoppen" Basic.Main.StoppingStreaming="Stoppe Stream..." Basic.Main.ForceStopStreaming="Streaming stoppen (Verzögerung verwerfen)" +Basic.Main.Group="Gruppe %1" +Basic.Main.GroupItems="Ausgewählte Elemente gruppieren" +Basic.Main.Ungroup="Gruppierung aufheben" Basic.MainMenu.File="Datei (&F)" Basic.MainMenu.File.Export="&Exportieren" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="Werkzeuge (&T)" Basic.MainMenu.Help="&Hilfe" Basic.MainMenu.Help.HelpPortal="Hilfe&portal" Basic.MainMenu.Help.Website="&Webseite besuchen" +Basic.MainMenu.Help.Discord="Tritt unserem &Discordserver bei" Basic.MainMenu.Help.Logs="&Logdateien" Basic.MainMenu.Help.Logs.ShowLogs="Logdateien anzeigen (&S)" Basic.MainMenu.Help.Logs.UploadCurrentLog="Upload aktuelle Logdatei (&C)" Basic.MainMenu.Help.Logs.UploadLastLog="Upload &letzte Logdatei" Basic.MainMenu.Help.Logs.ViewCurrentLog="Aktuelles Protokoll anzeigen (&V)" Basic.MainMenu.Help.CheckForUpdates="Auf Updates prüfen" +Basic.MainMenu.Help.CrashLogs="Abstu&rzberichte" +Basic.MainMenu.Help.CrashLogs.ShowLogs="Ab&sturzberichte anzeigen" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="Upload &letzten Absturzbericht" Basic.Settings.ProgramRestart="Das Programm muss neu gestartet werden, damit die Änderungen wirksam werden." Basic.Settings.ConfirmTitle="Änderungen bestätigen" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Immer zum Infobereich. statt zur Basic.Settings.General.SaveProjectors="Projektoren beim Beenden speichern" Basic.Settings.General.SwitchOnDoubleClick="Übergang zur Szene beim Doppelklicken" Basic.Settings.General.StudioPortraitLayout="Porträt/vertikales Layout aktivieren" +Basic.Settings.General.Multiview="Multiview" +Basic.Settings.General.Multiview.MouseSwitch="Klicken, um zwischen den Szenen umzuschalten" +Basic.Settings.General.Multiview.DrawSourceNames="Szenennamen anzeigen" +Basic.Settings.General.Multiview.DrawSafeAreas="Zeige sichere Bereiche (EBU R 95)" Basic.Settings.General.MultiviewLayout="Multiview Layout" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horizontal, oben" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horizontal, unten" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertikal, links" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertikal, rechts" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horizontal, oben (8 Szenen)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horizontal, unten (8 Szenen)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertikal, links (8 Szenen)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertikal, rechts (8 Szenen)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Horizontal, oben (24 Szenen)" Basic.Settings.Stream="Stream" Basic.Settings.Stream.StreamType="Stream Typ" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="Aussteuerungsmesserverfallsrate" Basic.Settings.Audio.MeterDecayRate.Fast="Schnell" Basic.Settings.Audio.MeterDecayRate.Medium="Mittel (Type I PPM)" Basic.Settings.Audio.MeterDecayRate.Slow="Langsam (Type II PPM)" +Basic.Settings.Audio.PeakMeterType="Peakmeter Typ" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Sample Peak" +Basic.Settings.Audio.PeakMeterType.TruePeak="True Peak (höhere CPU-Auslastung)" Basic.Settings.Audio.MultiChannelWarning.Enabled="WARNUNG: Surround-Sound-Audio ist aktiviert." Basic.Settings.Audio.MultichannelWarning="Überprüfen Sie beim Streaming, ob Ihr Streaming-Dienst sowohl die Einspeisung von Surround-Sound als auch die Surround-Sound-Wiedergabe unterstützt. Twitch, Facebook 360 Live, Mixer RTMP und Smashcast sind Beispiele, bei denen Surround Sound voll unterstützt wird. Obwohl Facebook Live und YouTube Live beide die Surround-Einspeisung akzeptieren, wird Facebook Live auf Stereo heruntergemischt und YouTube Live spielt nur zwei Kanäle ab.\n\nOBS-Audiofilter sind mit Surround-Sound kompatibel, obwohl die VST-Plugin-Unterstützung nicht garantiert ist." Basic.Settings.Audio.MultichannelWarning.Title="Surround-Sound-Audio aktivieren?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="Netzwerk" Basic.Settings.Advanced.Network.BindToIP="Interface" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Neuen Netzwerkcode aktivieren" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Niedriger Latenzmodus" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Hotkeys deaktivieren, wenn das Hauptfenster im Fokus ist" Basic.AdvAudio="Erweiterte Audioeigenschaften" Basic.AdvAudio.Name="Name" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="Warnung: Aufnahmen, die in MP4 gespeichert werden, FinalScene.Title="Szene löschen" FinalScene.Text="Es muss mindestens eine Szene vorhanden sein." +NoSources.Title="Keine Quellen" +NoSources.Text="Offenbar haben Sie noch keine Videoquellen hinzugefügt, so dass Sie nur einen leeren Bildschirm ausgeben werden. Sind Sie sicher, dass Sie das wollen?" +NoSources.Text.AddSource="Sie können zu jeder Zeit Quellen hinzufügen, indem Sie auf das + Symbol unter dem Quellenfeld im Hauptfenster klicken." + +ChangeBG="Farbe auswählen" +CustomColor="Benutzerdefinierte Farbe" + +BrowserSource.EnableHardwareAcceleration="Browser Hardwarebeschleunigung aktivieren" + diff --git a/UI/data/locale/el-GR.ini b/UI/data/locale/el-GR.ini index 17fe7ff..5c9954e 100644 --- a/UI/data/locale/el-GR.ini +++ b/UI/data/locale/el-GR.ini @@ -28,12 +28,16 @@ Browse="Αναζήτηση" Mono="Μονοφωνικό" Stereo="Στερεοφωνικό" DroppedFrames="Διακεκομμένα καρέ %1 (%2%)" +StudioProgramProjector="Προβολέας Πλήρους Οθόνης (Πηγή)" PreviewProjector="Προβολέας Πλήρους Οθόνης (Προεπισκόπηση)" SceneProjector="Προβολέας Πλήρους Οθόνης (Σκηνή)" SourceProjector="Προβολέας Πλήρους Οθόνης (Πηγή)" +StudioProgramWindow="Παραθύρου προβολής (πρόγραμμα)" PreviewWindow="Σε παράθυρο στον Προβολέα (Προεπισκόπηση)" SceneWindow="Σε παράθυρο στον Προβολέα (Σκηνή)" SourceWindow="Σε παράθυρο στον Προβολέα (Πηγή)" +MultiviewProjector="MultiView (πλήρης οθόνη)" +MultiviewWindowed="MultiView (παραθύρου)" Clear="Καθαρισμός" Revert="Επαναφορά" Show="Εμφάνιση" @@ -69,6 +73,11 @@ Next="Επόμενο" Back="Προηγούμενο" Defaults="Προεπιλογές" HideMixer="Απόκρυψη στον Μίκτη" +TransitionOverride="Παράκαμψη της μετάβασης" +None="Καμία" +StudioMode.Preview="Προεπισκόπηση" +StudioMode.Program="Πρόγραμμα" +ShowInMultiview="Εμφάνιση σε Multiview" AlreadyRunning.Title="Το OBS εκτελείται ήδη" AlreadyRunning.Text="Το OBS εκτελείται ήδη! Εκτός αν θέλατε να το κάνετε αυτό, παρακαλούμε τερματίστε τις τρέχουσες διεργασίες OBS πριν προσπαθήσετε να εκκινήσετε μια καινούρια. Εάν έχετε ρυθμίσει το OBS να ελαχιστοποιείται στην γραμμή εργαλείων, παρακαλούμε να ελένξετε αν τρέχει ήδη εκεί." @@ -221,6 +230,7 @@ Output.RecordNoSpace.Msg="Δεν υπάρχει επαρκής χώρος στο Output.RecordError.Title="Σφάλμα εγγραφής" Output.RecordError.Msg="Παρουσιάστηκε ένα αδιευκρίνιστο σφάλμα κατά την εγγραφή." Output.ReplayBuffer.NoHotkey.Title="Δεν έχει επιλεχθεί hotkey!" +Output.ReplayBuffer.NoHotkey.Msg="Καμμία αποθήκευση συντόμευσης για επανάληψη buffer. Παρακαλώ ορίστε την συντόμευση για «Αποθήκευση» καί χρήση για την αποθήκευση επανάληψης ηχογραφήσεων." Output.BadPath.Title="Λάθος Διαδρομή Αρχείου" Output.BadPath.Text="Η προκαθορισμένη διαδρομή αρχείου δεν ειναι έγκυρη. Παρακαλώ ελέγξτε τις ρυθμίσεις σας για να επιβεβαιώσετε ότι έχει οριστεί μια έγκυρη διαδρομή αρχείου." @@ -302,6 +312,8 @@ AddProfile.Text="Παρακαλώ εισάγετε το όνομα του προ RenameProfile.Title="Μετονομασία Προφίλ" +Basic.Main.MixerRename.Title="Μετονομασία της πηγής ήχου" +Basic.Main.MixerRename.Text="Παρακαλώ εισάγετε το όνομα της πηγής ήχου" Basic.Main.PreviewDisabled="Η προεπισκόπηση είναι απενεργοποιημένη" @@ -457,6 +469,7 @@ Basic.MainMenu.SceneCollection.Exists="Η συλλογή σκήνων υπάρχ Basic.MainMenu.Tools="&Εργαλεία" Basic.MainMenu.Help="Βοήθεια(&H)" +Basic.MainMenu.Help.HelpPortal="Πύλη βοήθειας" Basic.MainMenu.Help.Website="Μετάβαση στην &Ιστοσελίδα" Basic.MainMenu.Help.Logs="Αρχεία(&) Καταγραφής" Basic.MainMenu.Help.Logs.ShowLogs="Εμφάνιση(&S) Αρχείων Καταγραφής" @@ -476,6 +489,25 @@ Basic.Settings.General.EnableAutoUpdates="Αυτόματος έλεγχος εν Basic.Settings.General.OpenStatsOnStartup="Άνοιγμα παραθύρου στατιστικών κατά την εκκίνηση" Basic.Settings.General.WarnBeforeStartingStream="Εμφάνιση παραθύρου επιβεβαίωσης κατά την εκκίνηση των streams" Basic.Settings.General.WarnBeforeStoppingStream="Εμφάνιση παραθύρου επιβεβαίωσης κατά τη διακοπή των streams" +Basic.Settings.General.Projectors="Προβολείς" +Basic.Settings.General.HideProjectorCursor="Απόκρυψη τού δρομέα πάνω από τούς προβολείς" +Basic.Settings.General.ProjectorAlwaysOnTop="Προβολείς πάντα στην κορυφή" +Basic.Settings.General.Snapping="Ευθυγράμμιση πηγής" +Basic.Settings.General.ScreenSnapping="Προσκόλληση πηγών στην άκρη της οθόνης" +Basic.Settings.General.CenterSnapping="Προσκόλληση πηγών οριζόντια και κάθετα στο κέντρο" +Basic.Settings.General.SourceSnapping="Προσκόλληση πηγών σε άλλες πηγές" +Basic.Settings.General.SnapDistance="Ευαισθησία προσκόλλησης" +Basic.Settings.General.RecordWhenStreaming="Αυτόματη καταγραφή κατά την ροή" +Basic.Settings.General.KeepRecordingWhenStreamStops="Διατηρήσετε καταγραφών όταν σταματά η ροή" +Basic.Settings.General.ReplayBufferWhileStreaming="Αυτόματη εκκίνηση replay buffer κατά τη ροή" +Basic.Settings.General.KeepReplayBufferStreamStops="Διατήρηση επανάληψης buffer όταν σταματά η ροή" +Basic.Settings.General.SysTray="System Tray" +Basic.Settings.General.SysTrayWhenStarted="Ελαχιστοποίηση στο System Tray κατά την εκκίνηση" +Basic.Settings.General.SystemTrayHideMinimize="Συνεχής ελαχιστοποίηση στο System Tray αντί στην γραμμή εργασιών" +Basic.Settings.General.SaveProjectors="Αποθήκευση προβολής στην έξοδο" +Basic.Settings.General.SwitchOnDoubleClick="Μετάβαση στην σκηνή με διπλό κλικ" +Basic.Settings.General.StudioPortraitLayout="Ενεργοποίηση πορτρέτου/κατακόρυφης διάταξης" +Basic.Settings.General.MultiviewLayout="MultiView διάταξη" Basic.Settings.Stream="Μετάδοση" Basic.Settings.Stream.StreamType="Τύπος Μετάδοσης" @@ -485,24 +517,37 @@ Basic.Settings.Output.Format="Μορφή Καταγραφής" Basic.Settings.Output.Encoder="Κωδικοποιητής" Basic.Settings.Output.SelectDirectory="Επιλέξτε κατάλογο καταγραφής" Basic.Settings.Output.SelectFile="Επιλέξτε αρχείο καταγραφής" +Basic.Settings.Output.EnforceBitrate="Επιβολή ροής υπηρεσία bitrate ορίων" Basic.Settings.Output.Mode="Λειτουργία Εξόδου" Basic.Settings.Output.Mode.Simple="Απλό" Basic.Settings.Output.Mode.Adv="Σύνθετες επιλογές" Basic.Settings.Output.Mode.FFmpeg="Έξοδος FFmpeg" +Basic.Settings.Output.UseReplayBuffer="Ενεργοποίηση επανάληψης Buffer" Basic.Settings.Output.ReplayBuffer.SecondsMax="Μέγιστος Χρόνος Επανάληψης (Δευτερόλεπτα)" Basic.Settings.Output.ReplayBuffer.MegabytesMax="Μέγιστη Μνήμη (Megabytes)" Basic.Settings.Output.ReplayBuffer.Estimate="Εκτιμώμενη χρήση μνήμης: %1 MB" Basic.Settings.Output.ReplayBuffer.EstimateUnknown="Δεν είναι δυνατή η εκτίμηση της χρήσης μνήμης. Ορίστε μέγιστο όριο μνήμης." +Basic.Settings.Output.ReplayBuffer.HotkeyMessage="(Σημείωση: Βεβαιωθείτε ότι ορίσατε μία συντόμευση για το buffer στην ενότητα συντομέυσεων)" +Basic.Settings.Output.ReplayBuffer.Prefix="Πρόθεμα ονόματος αρχείου Buffer Replay" Basic.Settings.Output.ReplayBuffer.Suffix="Επίθεμα" Basic.Settings.Output.Simple.SavePath="Διαδρομή Καταγραφής" Basic.Settings.Output.Simple.RecordingQuality="Ποιότητα Εγγραφής" +Basic.Settings.Output.Simple.RecordingQuality.Stream="Ίδιο με την ροή" Basic.Settings.Output.Simple.RecordingQuality.Small="Υψηλής Ποιότητας, Μεσαίου Μεγέθους Αρχείο" Basic.Settings.Output.Simple.RecordingQuality.HQ="Δυσδιάκριτης Ποιότητας, Μεγάλου Μεγέθους Αρχείο" Basic.Settings.Output.Simple.RecordingQuality.Lossless="Ποιότητας Χωρίς Απώλειες, Εξαιρετικά Μεγάλου Μεγέθους Αρχείο" +Basic.Settings.Output.Simple.Warn.VideoBitrate="Προειδοποίηση: Το streaming βίντεο bitrate θα οριστεί %1, που είναι το ανώτερο όριο για την τρέχουσα υπηρεσία συνεχούς ροής. Εάν είστε βέβαιοι ότι θέλετε να πάτε πάνω από %1, Ενεργοποίηση επιλογών προηγμένο κωδικοποιητή και uncheck «Επιβολή streaming υπηρεσία bitrate όρια»." +Basic.Settings.Output.Simple.Warn.AudioBitrate="Προειδοποίηση: Η ροή ήχου bitrate θα οριστεί %1, που είναι το ανώτερο όριο για την τρέχουσα υπηρεσία συνεχούς ροής. Εάν είστε βέβαιοι ότι θέλετε να πάτε πάνω από %1, Ενεργοποίηση επιλογών προηγμένο κωδικοποιητή και uncheck «Επιβολή streaming υπηρεσία bitrate όρια»." +Basic.Settings.Output.Simple.Warn.Encoder="Προσοχή: Η εγγραφή με έναν κωδικοποιητή λογισμικού σε διαφορετική ποιότητα από την ροή θα απαιτήσει πρόσθετη χρήση της CPU, αν μπορείτε να πραγματοποιήσετε την ροή και την εγγραφή την ίδια στιγμή." +Basic.Settings.Output.Simple.Warn.Lossless="Προειδοποίηση: Η μη απωλεστική ποιότητα δημιουργεί τρομερά μεγάλο μέγεθος των αρχείων! Η ποιότητα χωρίς απώλειες ποιότητας θα καταλάβει 7 gigabyte χώρο στον σκληρό δίσκο ανά λεπτό, σε υψηλές αναλύσεις και framerates. Χωρίς απώλειες δεν συνιστάται για μεγάλες ηχογραφήσεις, εκτός αν έχετε πολύ χώρο στον σκληρό δίσκο." +Basic.Settings.Output.Simple.Warn.Lossless.Msg="Είστε σίγουρος ότι θέλετε να το χρησιμοποιήσετε χωρίς απώλειες ποιότητας;" +Basic.Settings.Output.Simple.Warn.Lossless.Title="Χωρίς απώλειες ποιότητας προειδοποίηση!" +Basic.Settings.Output.Simple.Warn.MultipleQSV="Προειδοποίηση: Δεν μπορείτε να χρησιμοποιήσετε πολλαπλούς ξεχωριστούς QSV κωδικοποιητές κατά την ροή και την εγγραφή την ίδια στιγμή. Αν θέλετε κάνετε stream και να καταγράψετε ταυτόχρονα, παρακαλείστε να αλλάξετε είτε την εγγραφή η την ροή τού κωδικοποιητή." Basic.Settings.Output.Simple.Encoder.Software="Λογισμικό (x264)" Basic.Settings.Output.Simple.Encoder.Hardware.QSV="Υλικού (QSV)" Basic.Settings.Output.Simple.Encoder.Hardware.AMD="Υλικού (AMD)" Basic.Settings.Output.Simple.Encoder.Hardware.NVENC="Υλικού (NVENC)" +Basic.Settings.Output.Simple.Encoder.SoftwareLowCPU="Λογισμικό (x264 χαμηλή χρήση CPU preset, αυξάνει το μέγεθος τού αρχείου)" Basic.Settings.Output.VideoBitrate="Ρυθμός Bit του Βίντεο" Basic.Settings.Output.AudioBitrate="Ρυθμός Bit του Ήχου" Basic.Settings.Output.Reconnect="Αυτόματη Επανασύνδεση" @@ -522,12 +567,15 @@ Basic.Settings.Output.Adv.Audio.Track1="Κομμάτι 1" Basic.Settings.Output.Adv.Audio.Track2="Κομμάτι 2" Basic.Settings.Output.Adv.Audio.Track3="Κομμάτι 3" Basic.Settings.Output.Adv.Audio.Track4="Κομμάτι 4" +Basic.Settings.Output.Adv.Audio.Track5="Κομμάτι 5" +Basic.Settings.Output.Adv.Audio.Track6="Κομμάτι 6" Basic.Settings.Output.Adv.Recording="Εγγραφή" Basic.Settings.Output.Adv.Recording.Type="Τύπος" Basic.Settings.Output.Adv.Recording.Type.Standard="Κανονικός" Basic.Settings.Output.Adv.Recording.Type.FFmpegOutput="Προσαρμοσμένη Έξοδος (FFmpeg)" Basic.Settings.Output.Adv.Recording.UseStreamEncoder="(Χρήση κωδικοποιητή ροής)" +Basic.Settings.Output.Adv.Recording.Filename="Μορφοποίηση ονόματος αρχείου" Basic.Settings.Output.Adv.Recording.OverwriteIfExists="Αντικατάσταση εάν το αρχείο υπάρχει" Basic.Settings.Output.Adv.FFmpeg.Type="Τύπος εξόδου FFmpeg" Basic.Settings.Output.Adv.FFmpeg.Type.URL="Έξοδος σε διεύθυνση URL" @@ -553,9 +601,12 @@ Basic.Settings.Output.Adv.FFmpeg.IgnoreCodecCompat="Προβολή όλων τω FilenameFormatting.completer="%AAXX-%MM-%ΗΗ %ωω-%λλ-%δδ\n%ΧΧ-%ΜΜ-%ΗΗ %ωω-%λλ-%δδ\n%Χ-%μ-%η %Ω-%Λ-%Δ\n%χ-%μ-%η %Ω-%Λ-%Δ\n%α %Χ-%μ-%η %Ω-%Λ-%Δ\n%Α %Χ-%μ-%η %Ω-%Λ-%Δ\n%Χ-%μ-%η %Ω-%Λ-%Δ\n%Χ-%Μ-%η %Ω-%Λ-%Δ\n%Χ-%μ-%η %Ω-%Λ-%Δ-%p\n%Χ-%μ-%μ %Ω-%Λ-%Δ-%ζ\n%Χ-%μ-%η %Ω-%Λ-%Δ-%Ζ" +FilenameFormatting.TT="%CCYY έτους, τέσσερις digits\n%YY έτος, τα δύο τελευταία ψηφία (00-99) \n%MM μήνα ως ένα δεκαδικό αριθμό (01-12) \n%DD ημέρα του μήνα, μηδέν επένδυση (01-31)\n%hh ώρα στο 24h μορφή (00-23) \n%mm λεπτό (00-59) \n%ss δεύτερος (00-61) \n%% μια%a sign\n % συντετμημένη καθημερινές name\n%A πλήρη ημέρα της εβδομάδας name\n%b συντετμημένη μήνα name\n%B πλήρη μήνα name\n%d ημέρα του μήνα, μηδέν-γεμισμένος (01-31) \n%H ώρα στο 24h μορφή (00-23) \n%I ώρα σε μορφή 12h (01-12)\n%m μήνας ως δεκαδικός αριθμός (01-12)\n%M λεπτά (00-59) \n%p π. μ. ή μ. μ. designation\n%S δεύτερο (00-61) \n%y έτος, τελευταία δύο ψηφία (00-99)\n%Y Year\n%z ISO 8601 μετατόπιση από UTC ή timezone\n όνομα ή όνομα ζώνης ώρας%Z abbreviation\n ή abbreviation\n" Basic.Settings.Video="Βίντεο" Basic.Settings.Video.Adapter="Προσαρμογέας Βίντεο:" +Basic.Settings.Video.BaseResolution="Βάση (Καμβάς) Ανάλυση" +Basic.Settings.Video.ScaledResolution="Ανάλυση εξόδου (κλίμακα)" Basic.Settings.Video.DownscaleFilter="Φίλτρο Σμίκρυνσης:" Basic.Settings.Video.DisableAeroWindows="Απενεργοποίηση Aero (Windows μόνο)" Basic.Settings.Video.FPS="Καρέ ανά δευτερόλεπτο (FPS):" @@ -576,23 +627,48 @@ Basic.Settings.Video.DownscaleFilter.Lanczos="Lanczos (Οξυμμένη κλιμ Basic.Settings.Audio="Ήχος" Basic.Settings.Audio.SampleRate="Ρυθμός Δειγματοληψίας" Basic.Settings.Audio.Channels="Κανάλια" +Basic.Settings.Audio.MeterDecayRate="Συντελεστής απόσβεσης ήχου μέτρησης" +Basic.Settings.Audio.MeterDecayRate.Fast="Γρήγορος" +Basic.Settings.Audio.MeterDecayRate.Medium="Μεσαίος (τύπος Ι PPM)" +Basic.Settings.Audio.MeterDecayRate.Slow="Αργός (Τύπος ΙΙ PPM)" +Basic.Settings.Audio.MultiChannelWarning.Enabled="Προειδοποίηση: Ο ήχος Surround είναι ενεργοποιημένος." +Basic.Settings.Audio.MultichannelWarning="Κατά το streaming, ελέγξτε αν η υπηρεσία streaming υποστηρίζει ήχο surround δύο ηχείων και ήχο surround αναπαραγωγής. Το Twitch, το Facebook 360 Live, το μίξερ RTMP καί το Smashcast αποτελούν παραδείγματα όπου ο surround ήχος υποστηρίζεται πλήρως. Αν και το Facebook Live και το YouTube Live αποδέχεστε τον ήχο surround, το Facebook Live κατεβάζει τον σε stereo, και το YouTube Live υποστηρίζει μόνο δύο φίλτρα ήχου.\n\nΤο OBS είναι συμβατό με ήχο surround, αν καί δεν είναι εγγυημένη η υποστήριξη για plugins VST." +Basic.Settings.Audio.MultichannelWarning.Title="Ενεργοποίηση ήχου surround;" +Basic.Settings.Audio.MultichannelWarning.Confirm="Είναι βέβαιοι ότι θέλετε να ενεργοποιήσετε τον ήχο surround;" Basic.Settings.Audio.DesktopDevice="Συσκευή Ήχου Επιφάνειας" Basic.Settings.Audio.DesktopDevice2="Συσκευή Ήχου Επιφάνειας 2" Basic.Settings.Audio.AuxDevice="Μικρόφωνο/Αuxillary Συσκευή Ήχου" Basic.Settings.Audio.AuxDevice2="Μικρόφωνο/Αuxillary Συσκευή Ήχου 2" Basic.Settings.Audio.AuxDevice3="Μικρόφωνο/Αuxillary Συσκευή Ήχου 3" +Basic.Settings.Audio.EnablePushToMute="Ενεργοποίηση της ώθησης-γιά-σίγαση" +Basic.Settings.Audio.PushToMuteDelay="Καθυστέρηση ώθησης-γιά-σίγαση" +Basic.Settings.Audio.EnablePushToTalk="Ενεργοποίηση Push-to-talk" +Basic.Settings.Audio.PushToTalkDelay="Push-to-talk καθυστέρηση" +Basic.Settings.Audio.UnknownAudioDevice="[Η συσκευή δεν είναι συνδεδεμένη η δεν είναι διαθέσιμη]" Basic.Settings.Advanced="Σύνθετες επιλογές" +Basic.Settings.Advanced.General.ProcessPriority="Προτεραιότητα διαδικασίας" +Basic.Settings.Advanced.General.ProcessPriority.High="Υψηλή" +Basic.Settings.Advanced.General.ProcessPriority.AboveNormal="Πάνω από το φυσιολογικό" +Basic.Settings.Advanced.General.ProcessPriority.Normal="Κανονική" +Basic.Settings.Advanced.General.ProcessPriority.BelowNormal="Κάτω από την κανονική" +Basic.Settings.Advanced.General.ProcessPriority.Idle="Σε αδράνεια" Basic.Settings.Advanced.FormatWarning="Προσοχή: Μορφές χρώματος εκτός του NV12 προορίζονται κυρίως για καταγραφή, και δεν συνιστώνται κατά τη μετάδοση. Ενδέχεται να υπάρξει αυξημένη χρήση της CPU λόγω μετατροπής μορφής χρώματος." Basic.Settings.Advanced.Audio.BufferingTime="Χρόνος buffering ήχου" Basic.Settings.Advanced.Video.ColorFormat="Μορφή Χρώματος" +Basic.Settings.Advanced.Video.ColorSpace="Χώρος χρωμάτων YUV" +Basic.Settings.Advanced.Video.ColorRange="Ποικιλία χρωμάτων YUV" Basic.Settings.Advanced.Video.ColorRange.Partial="Μερικό" Basic.Settings.Advanced.Video.ColorRange.Full="Πλήρες" +Basic.Settings.Advanced.Audio.MonitoringDevice="Συσκευή παρακολούθησης ήχου" Basic.Settings.Advanced.Audio.MonitoringDevice.Default="Προεπιλεγμένη" +Basic.Settings.Advanced.Audio.DisableAudioDucking="Απενεργοποίηση σίγασης ήχου" +Basic.Settings.Advanced.StreamDelay="Καθυστέρηση ροής" Basic.Settings.Advanced.StreamDelay.Duration="Διάρκεια (δευτερόλεπτα)" Basic.Settings.Advanced.StreamDelay.Preserve="Διατήρηση σημείου αποκοπής (αύξηση καθυστέρησης) κατά την επανασύνδεση" Basic.Settings.Advanced.StreamDelay.MemoryUsage="Εκτιμώμενη Χρήση Μνήμης: %1 MB" Basic.Settings.Advanced.Network="Δίκτυο" +Basic.Settings.Advanced.Network.BindToIP="Σύνδεση με IP" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Ενεργοποίηση νέου κώδικα δικτύωσης" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Λειτουργία χαμηλής καθυστέρησης" @@ -602,9 +678,14 @@ Basic.AdvAudio.Volume="Ένταση (%)" Basic.AdvAudio.Mono="Αποκωδικοποίηση σε Mono" Basic.AdvAudio.Panning="Πανοραμικό" Basic.AdvAudio.SyncOffset="Μετατόπιση Συγχρονισμού (ms)" +Basic.AdvAudio.Monitoring="Ηχητική παρακολούθηση" +Basic.AdvAudio.Monitoring.None="Monitor Off" +Basic.AdvAudio.Monitoring.MonitorOnly="Μόνο η οθόνη (σίγαση εξόδου)" +Basic.AdvAudio.Monitoring.Both="Παρακολούθηση και έξοδος" Basic.AdvAudio.AudioTracks="Κομμάτια" Basic.Settings.Hotkeys="Πλήκτρα συντόμευσης" +Basic.Settings.Hotkeys.Pair="Συνδυασμοί πλήκτρων που μοιράζεται με το «%1» που ενεργούν ως εναλλαγή" Basic.Hotkeys.SelectScene="Μετάβαση σε σκηνή" @@ -644,12 +725,26 @@ Hotkeys.AppleKeypadNum="%1 (Keypad)" Hotkeys.AppleKeypadMultiply="* (Keypad)" Hotkeys.AppleKeypadDivide="/ (Keypad)" Hotkeys.AppleKeypadAdd="+ (Keypad)" +Hotkeys.AppleKeypadSubtract="- (Αριθμητικό πληκτρολόγιο)" +Hotkeys.AppleKeypadDecimal=". (Πληκτρολόγιο)" +Hotkeys.AppleKeypadEqual="= (Αριθμητικό πληκτρολόγιο)" +Hotkeys.MouseButton="%1 ποντίκι" Mute="Σίγαση" Unmute="Κατάργηση σίγασης" +Push-to-mute="Ώθηση-για-σίγαση" +Push-to-talk="Πίεση και ομιλία" SceneItemShow="Εμφάνιση '%1'" SceneItemHide="Απόκρυψη '%1'" +OutputWarnings.NoTracksSelected="Πρέπει να επιλέξετε τουλάχιστον ένα κομμάτι" +OutputWarnings.MultiTrackRecording="Προειδοποίηση: Ορισμένες μορφές (όπως FLV) δεν υποστηρίζουν πολλαπλά κομμάτια ανά εγγραφή" +OutputWarnings.MP4Recording="Προειδοποίηση: Οι ηχογραφήσεις που έχουν αποθηκευτεί σε MP4 θα είναι αδιόρθωτες, αν το αρχείο δεν είναι δυνατόν να ολοκληρωθεί (π.χ. λόγω BSODs, απώλεια ισχύος, κλπ.). Αν θέλετε να καταγράψετε πολλαπλά κομμάτια ήχου χρησιμοποιήστε το MKV και remux καταγραφής για mp4, αφού τελειώσει (αρχείο-> Remux ηχογραφήσεις)" + +FinalScene.Title="Διαγραφή σκηνής" +FinalScene.Text="Πρέπει να υπάρχει τουλάχιστον μία σκηνή." + + diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 89bd0b1..ca6d6a7 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -83,6 +83,8 @@ None="None" StudioMode.Preview="Preview" StudioMode.Program="Program" ShowInMultiview="Show in Multiview" +VerticalLayout="Vertical Layout" +Group="Group" # warning if program already open AlreadyRunning.Title="OBS is already running" @@ -457,6 +459,9 @@ Basic.Main.StoppingReplayBuffer="Stopping Replay Buffer..." Basic.Main.StopStreaming="Stop Streaming" Basic.Main.StoppingStreaming="Stopping Stream..." Basic.Main.ForceStopStreaming="Stop Streaming (discard delay)" +Basic.Main.Group="Group %1" +Basic.Main.GroupItems="Group Selected Items" +Basic.Main.Ungroup="Ungroup" # basic mode file menu Basic.MainMenu.File="&File" @@ -529,6 +534,7 @@ Basic.MainMenu.Tools="&Tools" Basic.MainMenu.Help="&Help" Basic.MainMenu.Help.HelpPortal="Help &Portal" Basic.MainMenu.Help.Website="Visit &Website" +Basic.MainMenu.Help.Discord="Join &Discord Server" Basic.MainMenu.Help.Logs="&Log Files" Basic.MainMenu.Help.Logs.ShowLogs="&Show Log Files" Basic.MainMenu.Help.Logs.UploadCurrentLog="Upload &Current Log File" @@ -570,11 +576,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Always minimize to system tray in Basic.Settings.General.SaveProjectors="Save projectors on exit" Basic.Settings.General.SwitchOnDoubleClick="Transition to scene when double-clicked" Basic.Settings.General.StudioPortraitLayout="Enable portrait/vertical layout" +Basic.Settings.General.Multiview="Multiview" +Basic.Settings.General.Multiview.MouseSwitch="Click to switch between scenes" +Basic.Settings.General.Multiview.DrawSourceNames="Show scene names" +Basic.Settings.General.Multiview.DrawSafeAreas="Draw safe areas (EBU R 95)" Basic.Settings.General.MultiviewLayout="Multiview Layout" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horizontal, Top" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horizontal, Bottom" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertical, Left" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertical, Right" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horizontal, Top (8 Scenes)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horizontal, Bottom (8 Scenes)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertical, Left (8 Scenes)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertical, Right (8 Scenes)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Horizontal, Top (24 Scenes)" # basic mode 'stream' settings Basic.Settings.Stream="Stream" @@ -611,7 +622,6 @@ Basic.Settings.Output.Simple.Warn.Encoder="Warning: Recording with a software en Basic.Settings.Output.Simple.Warn.Lossless="Warning: Lossless quality generates tremendously large file sizes! Lossless quality can use upward of 7 gigabytes of disk space per minute at high resolutions and framerates. Lossless is not recommended for long recordings unless you have a very large amount of disk space available." Basic.Settings.Output.Simple.Warn.Lossless.Msg="Are you sure you want to use lossless quality?" Basic.Settings.Output.Simple.Warn.Lossless.Title="Lossless quality warning!" -Basic.Settings.Output.Simple.Warn.MultipleQSV="Warning: You cannot use multiple separate QSV encoders when streaming and recording at the same time. If you want to stream and record at the same time, please change either the recording encoder or the stream encoder." Basic.Settings.Output.Simple.Encoder.Software="Software (x264)" Basic.Settings.Output.Simple.Encoder.Hardware.QSV="Hardware (QSV)" Basic.Settings.Output.Simple.Encoder.Hardware.AMD="Hardware (AMD)" @@ -707,6 +717,9 @@ Basic.Settings.Audio.MeterDecayRate="Audio Meter Decay Rate" Basic.Settings.Audio.MeterDecayRate.Fast="Fast" Basic.Settings.Audio.MeterDecayRate.Medium="Medium (Type I PPM)" Basic.Settings.Audio.MeterDecayRate.Slow="Slow (Type II PPM)" +Basic.Settings.Audio.PeakMeterType="Peak Meter Type" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Sample Peak" +Basic.Settings.Audio.PeakMeterType.TruePeak="True Peak (Higher CPU usage)" Basic.Settings.Audio.MultiChannelWarning.Enabled="WARNING: Surround sound audio is enabled." Basic.Settings.Audio.MultichannelWarning="If streaming, check to see if your streaming service supports both surround sound ingest and surround sound playback. Twitch, Facebook 360 Live, Mixer RTMP, Smashcast are examples where surround sound is fully supported. Although Facebook Live and YouTube Live both accept surround ingest, Facebook Live downmixes to stereo, and YouTube Live plays only two channels.\n\nOBS audio filters are compatible with surround sound, though VST plugin support isn't guaranteed." Basic.Settings.Audio.MultichannelWarning.Title="Enable surround sound audio?" @@ -748,6 +761,7 @@ Basic.Settings.Advanced.Network="Network" Basic.Settings.Advanced.Network.BindToIP="Bind to IP" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Enable new networking code" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Low latency mode" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Disable hotkeys when main window is in focus" # advanced audio properties Basic.AdvAudio="Advanced Audio Properties" @@ -831,3 +845,15 @@ OutputWarnings.MP4Recording="Warning: Recordings saved to MP4 will be unrecovera # deleting final scene FinalScene.Title="Delete Scene" FinalScene.Text="There needs to be at least one scene." + +# no sources +NoSources.Title="No Sources" +NoSources.Text="It looks like you haven't added any video sources yet, so you will only be outputting a blank screen. Are you sure you want to do this?" +NoSources.Text.AddSource="You can add sources by clicking the + icon under the Sources box in the main window at any time." + +# Scene item color selection +ChangeBG="Set Color" +CustomColor="Custom Color" + +# Global settings for the browser source +BrowserSource.EnableHardwareAcceleration="Enable Browser Source Hardware Acceleration" diff --git a/UI/data/locale/es-ES.ini b/UI/data/locale/es-ES.ini index ac279b0..df65678 100644 --- a/UI/data/locale/es-ES.ini +++ b/UI/data/locale/es-ES.ini @@ -78,6 +78,8 @@ None="Ninguno" StudioMode.Preview="Vista previa" StudioMode.Program="Programa" ShowInMultiview="Mostrar en vista múltiple" +VerticalLayout="Interfaz Vertical" +Group="Grupo" AlreadyRunning.Title="OBS ya se está ejecutando" AlreadyRunning.Text="¡OBS ya se está ejecutando! A no ser que quieras hacer esto, por favor, cierra todas las ventanas de OBS antes de intentar iniciar una nueva. Si tienes configurado OBS para que se minimize a la barra de tareas, prueba a ver si sigue ejecutándose ahí." @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="Deteniendo la reproducción del búfer..." Basic.Main.StopStreaming="Detener Transmisión" Basic.Main.StoppingStreaming="Deteniendo la trasmisión..." Basic.Main.ForceStopStreaming="Parar Transmisión (descartar retardo)" +Basic.Main.Group="Grupo %1" +Basic.Main.GroupItems="Agrupar los elementos seleccionados" +Basic.Main.Ungroup="Desagrupar" Basic.MainMenu.File="&Archivo" Basic.MainMenu.File.Export="&Exportar" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="&Herramientas" Basic.MainMenu.Help="&Ayuda" Basic.MainMenu.Help.HelpPortal="Ayuda (&P)" Basic.MainMenu.Help.Website="Visitar Sitio &Web" +Basic.MainMenu.Help.Discord="Unirse a un servidor &Discord" Basic.MainMenu.Help.Logs="&Archivos de registro" Basic.MainMenu.Help.Logs.ShowLogs="Mostrar archivo&s de registro" Basic.MainMenu.Help.Logs.UploadCurrentLog="Carga &de archivo de registro actual" Basic.MainMenu.Help.Logs.UploadLastLog="Carga del &último archivo de registro" Basic.MainMenu.Help.Logs.ViewCurrentLog="&Ver registro actual" Basic.MainMenu.Help.CheckForUpdates="Comprobar Actualizaciones" +Basic.MainMenu.Help.CrashLogs="Informes de &error" +Basic.MainMenu.Help.CrashLogs.ShowLogs="&Mostrar informes de error" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="Subir el &último informe de error" Basic.Settings.ProgramRestart="El programa debe reiniciarse para que esta configuración surta efecto." Basic.Settings.ConfirmTitle="Confirmar cambios" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Minimizar siempre en la bandeja d Basic.Settings.General.SaveProjectors="Guardar los proyectores al salir" Basic.Settings.General.SwitchOnDoubleClick="Transición a la escena cuando se hace doble clic" Basic.Settings.General.StudioPortraitLayout="Habilitar la disposición horizontal/vertical" +Basic.Settings.General.Multiview="Vista Múltiple" +Basic.Settings.General.Multiview.MouseSwitch="Click para cambiar entre escenas" +Basic.Settings.General.Multiview.DrawSourceNames="Mostrar nombres de las escenas" +Basic.Settings.General.Multiview.DrawSafeAreas="Dibujar áreas seguras (EBU R 95)" Basic.Settings.General.MultiviewLayout="Prueba de multivisión" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horizontal, parte superior" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horizontal, parte inferior" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertical, izquierda" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertical, derecha" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horizontal, superior (8 Escenas)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horizontal, inferior (8 Escenas)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertical, izquierda (8 Escenas)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertical, derecha (8 Escenas)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Horizontal, superior (24 Escenas)" Basic.Settings.Stream="Emision" Basic.Settings.Stream.StreamType="Tipo de Emision" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="Tasa de decaimiento del medidor de audio" Basic.Settings.Audio.MeterDecayRate.Fast="Rápida" Basic.Settings.Audio.MeterDecayRate.Medium="Media (PPM de tipo I)" Basic.Settings.Audio.MeterDecayRate.Slow="Lenta (PPM de tipo II)" +Basic.Settings.Audio.PeakMeterType="Tipo de medidor de pico" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Pico de muestra" +Basic.Settings.Audio.PeakMeterType.TruePeak="True Peak (mayor uso de CPU)" Basic.Settings.Audio.MultiChannelWarning.Enabled="ADVERTENCIA: el audio de sonido envolvente está habilitado." Basic.Settings.Audio.MultichannelWarning="Si se está transmitiendo, compruebe si su servicio de transmisión admite la ingesta de sonido envolvente y la reproducción de sonido envolvente. Twitch, Facebook 360 Live, Mixer RTMP, Smashcast son ejemplos en los que el sonido envolvente es totalmente compatible. Aunque Facebook Live y YouTube Live aceptan la ingesta surround, Facebook Live mezcla a estéreo y YouTube Live solo reproduce dos canales.\n\nLos filtros de audio OBS son compatibles con sonido envolvente, aunque no se garantiza el soporte de complementos VST." Basic.Settings.Audio.MultichannelWarning.Title="¿Habilitar el audio de sonido envolvente?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="Red" Basic.Settings.Advanced.Network.BindToIP="Enlazar con IP" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Habilitar el nuevo código de red" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Modo de baja latencia" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Deshabilitar teclas de acceso rápido cuando la ventana principal se encuentre activa" Basic.AdvAudio="Propiedades de Audio avanzadas" Basic.AdvAudio.Name="Nombre" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="ADVERTENCIA: Las grabaciones guardadas en MP4 será FinalScene.Title="Eliminar escena" FinalScene.Text="Debe haber al menos una escena." +NoSources.Title="Sin recursos" +NoSources.Text="Parece que no has añadido ningún recurso de vídeo aún, así que estarás emitiendo una pantalla en blanco. ¿Estás seguro de que quieres hacer esto?" +NoSources.Text.AddSource="Puedes añadir recursos haciendo click en el icono de \"+\" debajo del menú Recursos en la ventana principal en cualquier momento." + +ChangeBG="Establecer color" +CustomColor="Color personalizado" + +BrowserSource.EnableHardwareAcceleration="Habilitar Aceleración de Hardware de Recurso en el Navegador" + diff --git a/UI/data/locale/et-EE.ini b/UI/data/locale/et-EE.ini index 75a3c84..694f6ad 100644 --- a/UI/data/locale/et-EE.ini +++ b/UI/data/locale/et-EE.ini @@ -31,6 +31,7 @@ DroppedFrames="Vahele jäetud kaadreid %1 (%2%)" PreviewProjector="Projektor täisekraanil (eelvaade)" SceneProjector="Projektor täisekraanil (stseen)" SourceProjector="Projektor täisekraanil (allikas)" +MultiviewProjector="Mitmikvaade (täisekraanil)" Clear="Eemalda" Revert="Tühista" Show="Näita" @@ -59,20 +60,30 @@ Export="Ekspordi" Copy="Kopeeri" Paste="Kleebi" PasteReference="Kleebi (Viide)" +PasteDuplicate="Kleebi (Koopia)" Next="Edasi" Back="Tagasi" +StudioMode.Preview="Eelvaade" AlreadyRunning.Title="OBS juba töötab" +BandwidthTest.Region.US="Ameerika Ühendriigid" +BandwidthTest.Region.EU="Euroopa" +BandwidthTest.Region.Asia="Aasia" +BandwidthTest.Region.Other="Muu" +Basic.AutoConfig.ApplySettings="Rakenda muutused" Basic.AutoConfig.StreamPage.Service="Teenus" Basic.AutoConfig.StreamPage.Server="Server" +Basic.AutoConfig.StreamPage.StreamKey="Voogedastuse võti" Basic.AutoConfig.StreamPage.StreamKey.LinkToSite="(Link)" +Basic.Stats="Statistika" Basic.Stats.CPUUsage="CPU kasutus" Basic.Stats.Status.Inactive="Inaktiivne" +Basic.Stats.Bitrate="Bitikiirus" Updater.Title="Uus värskendus saadaval" Updater.Text="Uus värskendus on saadaval:" @@ -465,3 +476,6 @@ Mute="Vaigista" + + + diff --git a/UI/data/locale/eu-ES.ini b/UI/data/locale/eu-ES.ini index 4e80c29..51e163d 100644 --- a/UI/data/locale/eu-ES.ini +++ b/UI/data/locale/eu-ES.ini @@ -78,6 +78,8 @@ None="Gabe" StudioMode.Preview="Aurreikusi" StudioMode.Program="Programa" ShowInMultiview="Erakutsi ikuspegi anitzean" +VerticalLayout="Diseinu bertikala" +Group="Taldea" AlreadyRunning.Title="OBS dagoeneko martxan dago" AlreadyRunning.Text="OBS dagoeneko martxan dago! Bestelakorik nahi ez baduzu Itxi irekita dagoen saioa beste saio bat ireki baino lehen. Ezarri baduzu OBS agertzea minimizatua sistemaren erretiluan begiratu eta oraindik exekutatzen ari den bertan." @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="Erreprodukzio bufferra gelditzen..." Basic.Main.StopStreaming="Gelditu transmisioa" Basic.Main.StoppingStreaming="Transmisioa gelditzen..." Basic.Main.ForceStopStreaming="Gelditu transmisioa (baztertu atzerapena)" +Basic.Main.Group="%1 taldea" +Basic.Main.GroupItems="Batu hautatutako elementuak" +Basic.Main.Ungroup="Banatu" Basic.MainMenu.File="&Fitxategia" Basic.MainMenu.File.Export="&Esportatu" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="&Tresnak" Basic.MainMenu.Help="&Laguntza" Basic.MainMenu.Help.HelpPortal="Laguntza ataria" Basic.MainMenu.Help.Website="Ikusi &webgunea" +Basic.MainMenu.Help.Discord="Bat egin Discord zerbitzariarekin" Basic.MainMenu.Help.Logs="&Egunkari-fitxategiak" Basic.MainMenu.Help.Logs.ShowLogs="&Erakutsi egunkari-fitxategiak" Basic.MainMenu.Help.Logs.UploadCurrentLog="Kargatu &uneko egunkari-fitxategiak" Basic.MainMenu.Help.Logs.UploadLastLog="Kargatu &azken egunkari-fitxategia" Basic.MainMenu.Help.Logs.ViewCurrentLog="&Ikusi uneko egunkari-fitxategia" Basic.MainMenu.Help.CheckForUpdates="Begiratu eguneraketak" +Basic.MainMenu.Help.CrashLogs="Matxuren jakinarazpenak" +Basic.MainMenu.Help.CrashLogs.ShowLogs="Erakutsi matxuren jakinarazpenak" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="Kargatu azken matxura jakinarazpena" Basic.Settings.ProgramRestart="Programa berrabiarazi egin behar da ezarpen hauek eragina izateko." Basic.Settings.ConfirmTitle="Baieztatu aldaketak" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Minimizatu beti sistemaren erreti Basic.Settings.General.SaveProjectors="Gorde proiekzioak irtetean" Basic.Settings.General.SwitchOnDoubleClick="Aldatu eszena klik bikoitza egitean" Basic.Settings.General.StudioPortraitLayout="Gaitu diseinu horizontala/bertikala" +Basic.Settings.General.Multiview="Ikuspegi anitza" +Basic.Settings.General.Multiview.MouseSwitch="Klikatu eszena batetik bestera pasatzeko" +Basic.Settings.General.Multiview.DrawSourceNames="Erakutsi eszenen izenak" +Basic.Settings.General.Multiview.DrawSafeAreas="Markatu area seguruak (EBU R 95)" Basic.Settings.General.MultiviewLayout="Ikuspegi anitzeko diseinua" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horizontala, goian" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horizontala, behean" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Bertikala, ezkerrean" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Bertikala, eskuinean" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horizontala, goian (8 eszena)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horizontala, behean (8 eszena)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Bertikala, ezkerrean (8 eszena)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Bertikala, eskuinean (8 eszena)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Horizontala, goian (24 eszena)" Basic.Settings.Stream="Transmisioa" Basic.Settings.Stream.StreamType="Transmisio-mota" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="Audio neurtzailearen gutxiagotze-tasa" Basic.Settings.Audio.MeterDecayRate.Fast="Azkarra" Basic.Settings.Audio.MeterDecayRate.Medium="Tartekoa (I motako PPMa)" Basic.Settings.Audio.MeterDecayRate.Slow="Geldoa (II motako PPMa)" +Basic.Settings.Audio.PeakMeterType="Gailurren neurgailu mota" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Lagin-gailurra" +Basic.Settings.Audio.PeakMeterType.TruePeak="Benetako gailurra (CPUaren erabilera handiagoa)" Basic.Settings.Audio.MultiChannelWarning.Enabled="Kontu: soinu inguratzailea aktibatuta dago." Basic.Settings.Audio.MultichannelWarning="Transmititzen ari bazara, begiratu ea zure transmisio zerbitzuak onartzen duen soinu inguratzailea sarrerako soinuan zein irteerakoan. Twitch, Facebook 360 LIve, Mixer RTMP, Samashcast esate baterako guztiz onartzen dute soinu inguratzailea. Facebook Live eta Youtube Live sarrerako soinu inguratzailea onartzen badute ere, Facebook Livek estereo bihurtzen du, eta Youtube Livek bakarrik bi kanal erreproduzitzen ditu.\n\nOBS audio iragazkiak soinu inguratzailearekin bateragarriak badira ere, ezin da bermatu VST pluginaren bateragarritasuna." Basic.Settings.Audio.MultichannelWarning.Title="Nahi duzu soinu inguratzailea aktibatzea?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="Sarea" Basic.Settings.Advanced.Network.BindToIP="IP bidez lotu" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Gaitu sare kode berria" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Latentzia txikiko modua" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Desgaitu laster-teklak leiho nagusia fokuan dagoenean" Basic.AdvAudio="Audio propietate aurreratuak" Basic.AdvAudio.Name="Izena" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="Kontuz: MP4 formatuz gordetako grabazioak izan dait FinalScene.Title="Ezabatu eszena" FinalScene.Text="Gutxienez eszena bat egon behar du." +NoSources.Title="Iturbururik ez dago" +NoSources.Text="Badirudi ez duzula gehitu bideo iturbururik oraindik, beraz emaitza pantaila huts bat izango da. Ziur zaude hau egin nahi duzula?" +NoSources.Text.AddSource="Gehitzen ahal duzu iturburuak Iturburuak kutxako azpiko aldeko + ikonoa klikatuz edozein unetan." + +ChangeBG="Ezarri kolorea" +CustomColor="Kolore pertsonalizatua" + +BrowserSource.EnableHardwareAcceleration="Gaitu nabigatzailearen iturburuko hardware azelerazioa" + diff --git a/UI/data/locale/fa-IR.ini b/UI/data/locale/fa-IR.ini new file mode 100644 index 0000000..8593961 --- /dev/null +++ b/UI/data/locale/fa-IR.ini @@ -0,0 +1,148 @@ + + +OK="باشه" +Apply="اعمال تغییرات" +Cancel="لغو" +Close="ببند" +Save="ذخیره" +Discard="دور انداختن" +Disable="غیرفعال کردن" +Yes="بله" +No="خير" +Add="اضافه کردن" +Remove="حذف" +Rename="تغییر نام" +Interact="تعامل" +Filters="فیلتر ها" +Properties="تنظیمات" +MoveUp="انتقال به بالا" +MoveDown="انتقال به پایین" +Settings="تنظیمات" +Display="صفحه نمایش" +Name="نام" +Exit="خروج" +Mixer="میکسر" +Browse="تغییر" +Mono="مونو" +Stereo="استریو" +DroppedFrames="فریم های از دست رفته %1 (%2٪)" +StudioProgramProjector="پروژکتور تمام صفحه (برنامه)" +PreviewProjector="پروژکتور تمام صفحه (پیش نمایش)" +SceneProjector="پروژکتور تمام صفحه (صحنه)" +SourceProjector="پروژکتور تمام صفحه (منبع)" +StudioProgramWindow="پروژکتور پنجره ای (برنامه)" +PreviewWindow="پروژکتور پنجره ای (پیش نمایش)" +SceneWindow="پروژکتور پنجره ای (صحنه)" +SourceWindow="پروژکتور پنجره ای (منبع)" +MultiviewProjector="حالت چند نمایی (تمام صفحه)" +MultiviewWindowed="حالت چند نمایی (پنجره ای)" +Clear="پاک کردن" +Revert="برگرداندن" +Show="نمایش" +Hide="پنهان کردن" +UnhideAll="نمایش تمام مخفی ها" +Untitled="بدون عنوان" +New="ایجاد" +Duplicate="ایجاد مشابه" +Enable="فعال کردن" +DisableOSXVSync="غیرفعال کردن OSX V-Sync" +ResetOSXVSyncOnExit="تنظیم مجدد OSX V-Sync هنگام خروج" +HighResourceUsage="کد گذاری بیش از حد ! توجه کنید که تنظیمات ویدئویی را تغییر بدهید یا از یک پریست کد گذاری سریع تر استفاده کنید ." + + + + + +Basic.AutoConfig.VideoPage.BaseResolution.UseCurrent="استفاده از فعلی (%1x%2)" +Basic.AutoConfig.VideoPage.BaseResolution.Display="صفحه نمایش %1 (%2x%3)" +Basic.AutoConfig.VideoPage.FPS.UseCurrent="استفاده از فعلی (%1)" +Basic.AutoConfig.VideoPage.FPS.PreferHighFPS="یا ۶۰ یا ۳۰ ، اما ۶۰ را ترجیح می دهم" +Basic.AutoConfig.VideoPage.FPS.PreferHighRes="یا ۶۰ یا ۳۰ ، اما وضوح بالا را ترجیح می دهم" +Basic.AutoConfig.VideoPage.CanvasExplanation="نکته : اندازه محیط (پایه) لزوما همان اندازه ای نیست که با آن پخش زنده یا ضبط می کنید . اندازه واقعی پخش زنده/ضبط شما ممکن است برای کاهش استفاده از منابع و یا میزان درخواست بیت بر ثانیه (بیت ریت) کمتر باشد ." +Basic.AutoConfig.StreamPage="اطلاعات پخش زنده" +Basic.AutoConfig.StreamPage.SubTitle="لطفا اطلاعات پخش زنده خود را وارد کنید" +Basic.AutoConfig.StreamPage.Service="سرویس" +Basic.AutoConfig.StreamPage.Service.ShowAll="نمایش همه..." +Basic.AutoConfig.StreamPage.Server="سرور" +Basic.AutoConfig.StreamPage.StreamKey="کلید پخش زنده" +Basic.AutoConfig.StreamPage.StreamKey.LinkToSite="(لینک)" +Basic.AutoConfig.StreamPage.PerformBandwidthTest="برآورد میزان بیت بر ثانیه (بیت ریت) با تست پهنای باند (ممکن است چند دقیقه طول بکشد)" +Basic.AutoConfig.StreamPage.PreferHardwareEncoding="کد گذاری سخت افزاری را ترجیح می دهم" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/data/locale/fi-FI.ini b/UI/data/locale/fi-FI.ini index d9b37b4..8e72cbc 100644 --- a/UI/data/locale/fi-FI.ini +++ b/UI/data/locale/fi-FI.ini @@ -78,6 +78,8 @@ None="Ei mitään" StudioMode.Preview="Esikatselu" StudioMode.Program="Ohjelma" ShowInMultiview="Näytä moninäkymässä" +VerticalLayout="Pystynäkymä" +Group="Ryhmitä" AlreadyRunning.Title="OBS on jo käynnissä" AlreadyRunning.Text="OBS on jo käynnissä! Ellet tarkoittanut tehdä näin, ole hyvä ja sulje aikaisemmat OBS-prosessit ennen uuden käynnistämistä. Jos olet asettanut OBS:n pienentymään ilmaisinalueelle, varmista ettei se ole siellä yhä päällä." @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="Pysäytetään toistopuskuri..." Basic.Main.StopStreaming="Pysäytä lähetys" Basic.Main.StoppingStreaming="Pysäytetään lähetystä..." Basic.Main.ForceStopStreaming="Lopeta lähetys (ohita viive)" +Basic.Main.Group="Ryhmitä %1" +Basic.Main.GroupItems="Ryhmitä valitut lähteet" +Basic.Main.Ungroup="Poista ryhmästä" Basic.MainMenu.File="&Tiedosto" Basic.MainMenu.File.Export="&Vie" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="T&yökalut" Basic.MainMenu.Help="&Apua" Basic.MainMenu.Help.HelpPortal="&Apukeskus" Basic.MainMenu.Help.Website="Käy &verkkosivulla" +Basic.MainMenu.Help.Discord="Liity &Discord-palvelimelle" Basic.MainMenu.Help.Logs="&Lokitiedostot" Basic.MainMenu.Help.Logs.ShowLogs="&Näytä lokitiedostot" Basic.MainMenu.Help.Logs.UploadCurrentLog="Lähetä n&ykyinen lokitiedosto" Basic.MainMenu.Help.Logs.UploadLastLog="Lähetä edellinen lokitiedosto" Basic.MainMenu.Help.Logs.ViewCurrentLog="Näytä ny&kyinen loki" Basic.MainMenu.Help.CheckForUpdates="Tarkista päivitykset" +Basic.MainMenu.Help.CrashLogs="&Kaatumisraportit" +Basic.MainMenu.Help.CrashLogs.ShowLogs="&Näytä kaatumisraportit" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="&Lähetä kaatumisraportti" Basic.Settings.ProgramRestart="Ohjelma on käynnistettävä uudelleen, jotta asetukset tulevat voimaan." Basic.Settings.ConfirmTitle="Vahvista muutokset" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Pienennä aina tilapalkkiin teht Basic.Settings.General.SaveProjectors="Tallenna peilaus poistuessa" Basic.Settings.General.SwitchOnDoubleClick="Siirtymä skeneen tuplaklikattaessa" Basic.Settings.General.StudioPortraitLayout="Ota pystyasettelu käyttöön" +Basic.Settings.General.Multiview="Moninäkymä" +Basic.Settings.General.Multiview.MouseSwitch="Klikkaa vaihtaaksesi skenejen välillä" +Basic.Settings.General.Multiview.DrawSourceNames="Näytä skenejen nimet" +Basic.Settings.General.Multiview.DrawSafeAreas="Piirrä turva-alueet (EBU R 95)" Basic.Settings.General.MultiviewLayout="Moninäkymän asettelu" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Vaaka, ylhäällä" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Vaaka, alhaalla" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Pysty, vasemmalla" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Pysty, oikealla" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Vaakasuunta, ylhäällä (8 skeneä)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Vaakasuunta, alhaalla (8 skeneä)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Pystysuunta, vasemmalla (8 skeneä)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Pystysuunta, oikealla (8 skeneä)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Vaakasuunta, ylhäällä (24 skeneä)" Basic.Settings.Stream="Lähetys" Basic.Settings.Stream.StreamType="Lähetystyyppi" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="Äänimittarin putoamisnopeus" Basic.Settings.Audio.MeterDecayRate.Fast="Nopea" Basic.Settings.Audio.MeterDecayRate.Medium="Keskinopea (Tyyppi I PPM)" Basic.Settings.Audio.MeterDecayRate.Slow="Hidas (Tyyppi II PPM)" +Basic.Settings.Audio.PeakMeterType="Huippuarvo-mittarin tyyppi" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Huippuarvon näyte" +Basic.Settings.Audio.PeakMeterType.TruePeak="Todellinen huippuarvo (Korkeampi CPU:n käyttö)" Basic.Settings.Audio.MultiChannelWarning.Enabled="VAROITUS: Monikanavaääni on käytössä." Basic.Settings.Audio.MultichannelWarning="Varmista lähettäessä että palvelu tukee sekä monikanavaäänen lähettämistä, että toistamista. Twitch, Facebook 360 Live, Mixer RTMP ja Smashcast ovat esimerkkejä palveluista joissa monikanavaääni on täysin tuettu. Vaikka Facebook Live ja YouTube Live hyväksyvät monikanavaäänen lähettämisen, Facebook Live miksaa äänen stereoksi ja YouTube Live toistaa vain kaksi kanavaa.\n\nOBS:n äänisuodattimet tukevat monikanavaääntä, mutta VST-liitännäiset eivät välttämättä tue." Basic.Settings.Audio.MultichannelWarning.Title="Käytä monikanava-ääntä?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="Verkko" Basic.Settings.Advanced.Network.BindToIP="Liitä IP:seen" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Käytä uutta verkkokoodia" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Alhaisen latenssin tila" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Poista pikanäppäimet käytöstä, kun pääikkuna on aktiivisena" Basic.AdvAudio="Äänen lisäominaisuudet" Basic.AdvAudio.Name="Nimi" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="Varoitus: MP4-muotoon tallentaessa tiedostoista tul FinalScene.Title="Poista skene" FinalScene.Text="Ainakin yksi skene pitää olla olemassa." +NoSources.Title="Ei lähteitä" +NoSources.Text="Näyttää siltä ettet ole vielä lisännyt yhtään kuvalähdettä, joten kuva on musta. Haluatko varmasti tehdä näin?" +NoSources.Text.AddSource="Voit lisätä lähteitä klikkaamalla \"+\"-kuvaketta \"Lähteet\"-alueen alapuolella." + +ChangeBG="Aseta väri" +CustomColor="Mukautettu väri" + +BrowserSource.EnableHardwareAcceleration="Käytä laitteistokiihdytystä \"Selain\"-lähteessä" + diff --git a/UI/data/locale/fil-PH.ini b/UI/data/locale/fil-PH.ini index f39fa56..80ec529 100644 --- a/UI/data/locale/fil-PH.ini +++ b/UI/data/locale/fil-PH.ini @@ -76,6 +76,8 @@ None="Wala" StudioMode.Preview="Balikan" StudioMode.Program="Programa" ShowInMultiview="Ipakita sa Multiview" +VerticalLayout="Patayong Layout" +Group="Grupo" AlreadyRunning.Title="Tumatakbo na ngayon ang OBS" AlreadyRunning.Text="Tumatakbo na ang OBS! Maliban na lamang kung gusto mong gawin ito, pakiusap patayin ang anomang nabubuhay na mga mungkahi ng OBS bago subukang magpatakbo ng panibagong mungkahi. Kung meron kang OBS set para mabawasan ang sistemang tray, pakiusap magsiyasat para makita kung ito ay tumatakbo parin." @@ -402,6 +404,9 @@ Basic.Main.StoppingReplayBuffer="Pagtigil sa Pagre-Replay Buffer..." Basic.Main.StopStreaming="Itiigil ang Pag-stream" Basic.Main.StoppingStreaming="Pagtigil sa Pag-stream..." Basic.Main.ForceStopStreaming="Itigil ang Pag-stream (Iwaksi ang Pagkaantala)" +Basic.Main.Group="Grupo %1" +Basic.Main.GroupItems="I-grupo ang napiling mga aytem" +Basic.Main.Ungroup="Alisin sa Grupo" Basic.MainMenu.File="&Talaksan" Basic.MainMenu.File.Export="&I-export" @@ -468,6 +473,7 @@ Basic.MainMenu.Tools="&Mga Kasangkapan" Basic.MainMenu.Help="&Tulong" Basic.MainMenu.Help.HelpPortal="Tulong &lagusan" Basic.MainMenu.Help.Website="Pagbisita &website" +Basic.MainMenu.Help.Discord="Sumali sa &Discord Server" Basic.MainMenu.Help.Logs="Mag-&log ng mga File" Basic.MainMenu.Help.Logs.ShowLogs="&ipakita ang Pag-log ng mga File" Basic.MainMenu.Help.Logs.UploadCurrentLog="Mag-upload &Kasalukuyang Mag-log ng File" @@ -505,10 +511,6 @@ Basic.Settings.General.SaveProjectors="I-save ang mga prodyektor sa labasan" Basic.Settings.General.SwitchOnDoubleClick="Paglipat sa eksena kahit makadalawang-pindot" Basic.Settings.General.StudioPortraitLayout="Paganahin ang larawan/vertical layout" Basic.Settings.General.MultiviewLayout="Multiview Layout" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Pahalang, Itaas" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Pahalang, Pababa" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertical, Sa kaliwa" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertical, sa kanan" Basic.Settings.Stream="Stream" Basic.Settings.Stream.StreamType="Mga uri ng Stream" @@ -746,3 +748,6 @@ OutputWarnings.MP4Recording="Babala: Ang Recording na naka-save sa MP4 ay hindi FinalScene.Title="Tanggaling ang Eksena" FinalScene.Text="Doon kailangan ng kahit isang eksena." + + + diff --git a/UI/data/locale/fr-FR.ini b/UI/data/locale/fr-FR.ini index 17e52e0..0e12f98 100644 --- a/UI/data/locale/fr-FR.ini +++ b/UI/data/locale/fr-FR.ini @@ -78,6 +78,8 @@ None="Aucune" StudioMode.Preview="Aperçu" StudioMode.Program="Programme" ShowInMultiview="Montrer en Multivues" +VerticalLayout="Disposition Verticale" +Group="Groupe" AlreadyRunning.Title="OBS est déjà en cours d'exécution" AlreadyRunning.Text="OBS est déjà en cours d'exécution, merci de bien fermer toute autre instances existantes d'OBS avant d'en exécuter une nouvelle. Vérifiez dans votre barre d'état s'il n'est pas réduit et en cours d’exécution." @@ -92,7 +94,7 @@ BandwidthTest.Region.EU="Europe" BandwidthTest.Region.Asia="Asie" BandwidthTest.Region.Other="Autre" -Basic.FirstStartup.RunWizard="Exécutez l'Assistant de configuration ? Vous pouvez configurer manuellement vos paramètre en cliquant sur le bouton des Paramètres situer dans la fenêtre principale." +Basic.FirstStartup.RunWizard="Exécuter l'Assistant de configuration ? Vous pouvez configurer manuellement vos paramètres en cliquant sur le bouton des Paramètres situé dans la fenêtre principale." Basic.FirstStartup.RunWizard.BetaWarning="(Remarque : l’Assistant de configuration automatique est actuellement en version bêta)" Basic.FirstStartup.RunWizard.NoClicked="Si vous changez d’avis, vous pouvez réexécuter l’Assistant de configuration automatique n’importe quel moment dans le menu outils." @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="Arrêt du tampon de relecture..." Basic.Main.StopStreaming="Arrêter le streaming" Basic.Main.StoppingStreaming="Arrêt du stream..." Basic.Main.ForceStopStreaming="Arrêter le streaming (annule le retard)" +Basic.Main.Group="Groupe %1" +Basic.Main.GroupItems="Grouper les éléments sélectionnés" +Basic.Main.Ungroup="Dissocier" Basic.MainMenu.File="&Fichier" Basic.MainMenu.File.Export="&Exporter" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="Outils" Basic.MainMenu.Help="&Aide" Basic.MainMenu.Help.HelpPortal="&Portail d'aide" Basic.MainMenu.Help.Website="Consulter le site &Web" +Basic.MainMenu.Help.Discord="Rejoindre le serveur &Discord" Basic.MainMenu.Help.Logs="&Fichiers journaux" Basic.MainMenu.Help.Logs.ShowLogs="Afficher les &fichiers de log" Basic.MainMenu.Help.Logs.UploadCurrentLog="Mettre en ligne le fichier journal &actuel" Basic.MainMenu.Help.Logs.UploadLastLog="Mettre en ligne le &dernier fichier journal" Basic.MainMenu.Help.Logs.ViewCurrentLog="&Voir le journal actuel" Basic.MainMenu.Help.CheckForUpdates="Rechercher des mises à jour" +Basic.MainMenu.Help.CrashLogs="Rapports d'e&rreurs" +Basic.MainMenu.Help.CrashLogs.ShowLogs="Montrer les rapport&s d'erreur" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="Envoyer &le Dernier Rapport d'Erreur" Basic.Settings.ProgramRestart="Le programme doit être redémarré pour que les paramètres prennent effet." Basic.Settings.ConfirmTitle="Valider les modifications" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Toujours réduire dans la zone de Basic.Settings.General.SaveProjectors="Enregistrer les projecteurs en quittant" Basic.Settings.General.SwitchOnDoubleClick="Effectuer la transition vers la scène en cas de double clic" Basic.Settings.General.StudioPortraitLayout="Activer la mise en page portrait/verticale" +Basic.Settings.General.Multiview="Multivues" +Basic.Settings.General.Multiview.MouseSwitch="Cliquez pour changer de scène" +Basic.Settings.General.Multiview.DrawSourceNames="Montrer les noms des scènes" +Basic.Settings.General.Multiview.DrawSafeAreas="Afficher les zones sûres (EBU R 95)" Basic.Settings.General.MultiviewLayout="Disposition de la multivue" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horizontal, haut" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horizontal, bas" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertical, gauche" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertical, droit" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horizontal, Haut (8 Scènes)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horizontal, Bas (8 Scènes)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertical, Gauche (8 Scènes)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertical, Droite (8 Scènes)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Horizontal, Haut (24 Scènes)" Basic.Settings.Stream="Flux" Basic.Settings.Stream.StreamType="Type de diffusion" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="Vitesse de dégradation audiométrique" Basic.Settings.Audio.MeterDecayRate.Fast="Rapide" Basic.Settings.Audio.MeterDecayRate.Medium="Moyenne (Type I PPM)" Basic.Settings.Audio.MeterDecayRate.Slow="Lente (Type II PPM)" +Basic.Settings.Audio.PeakMeterType="Type de crête-mètre" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Pic d'échantillon" +Basic.Settings.Audio.PeakMeterType.TruePeak="Crête vraie (plus grande utilisation du CPU)" Basic.Settings.Audio.MultiChannelWarning.Enabled="ATTENTION : le son multicanal est activé." Basic.Settings.Audio.MultichannelWarning="Pour de besoins de diffusion, vérifiez que votre service de diffusion supporte l'intégration et la lecture du son multicanal. Twitch, Facebook 360 Live, Mixer RTMP ou Smashcast sont des exemples de services où le son multicanal est entièrement supporté. Bien que Facebook Live et YouTube Live acceptent l'intégration de son multicanal, Facebook Live transcode en stéréo, et YouTube Live ne lit que deux canaux.\n\nLes filtres audio d'OBS sont compatibles avec le son multicanal, toutefois le support du plugin VST n'est pas garanti." Basic.Settings.Audio.MultichannelWarning.Title="Activer le son multicanal ?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="Carte réseau (adresse IP source du flux)" Basic.Settings.Advanced.Network.BindToIP="Lier à :" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Activer le nouveau code réseau" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Mode faible latence" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Désactiver les raccourcis clavier lorsque la fenêtre principale est au premier plan" Basic.AdvAudio="Propriétés audio avancées" Basic.AdvAudio.Name="Nom" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="Avertissement : les enregistrements sauvegardés en FinalScene.Title="Supprimer la scène" FinalScene.Text="Il doit y avoir au moins une scène." +NoSources.Title="Aucune source" +NoSources.Text="Il semble que vous n'ayez pas ajouté de source vidéo pour le moment, vous aurez donc un écran noir. Êtes-vous sûr de vouloir continuer ?" +NoSources.Text.AddSource="Vous pouvez à tout moment ajouter des sources en cliquant sur l'icone + sous le bloc Sources de la fenêtre principale." + +ChangeBG="Définir la couleur" +CustomColor="Couleur personnalisée" + +BrowserSource.EnableHardwareAcceleration="Activer l’accélération matériel de la source navigateur" + diff --git a/UI/data/locale/gd-GB.ini b/UI/data/locale/gd-GB.ini new file mode 100644 index 0000000..0ea4d86 --- /dev/null +++ b/UI/data/locale/gd-GB.ini @@ -0,0 +1,770 @@ + +Language="Gàidhlig" +Region="Alba" + +OK="Ceart ma-thà" +Apply="Cuir an sàs" +Cancel="Sguir dheth" +Close="Dùin" +Save="Sàbhail" +Discard="Tilg air falbh" +Disable="Cuir à comas" +Yes="Tha" +No="Chan eil" +Add="Cuir ris" +Remove="Thoir air falbh" +Rename="Thoir ainm ùr air" +Interact="Eadar-ghnìomhach" +Filters="Criathragan" +Properties="Roghainnean" +MoveUp="Gluais suas" +MoveDown="Gluais sìos" +Settings="Roghainnean" +Display="Uidheam-taisbeanaidh" +Name="Ainm" +Exit="Fàg an-seo" +Mixer="Measgadair" +Browse="Rùraich" +Mono="Mono" +Stereo="Stereo" +DroppedFrames="Frèamaichean a thuit: %1 (%2%)" +StudioProgramProjector="Proiseactar làn-sgrìn (prògram)" +PreviewProjector="Proiseactar làn-sgrìn (ro-shealladh)" +SceneProjector="Proiseactar làn-sgrìn (sealladh)" +SourceProjector="Proiseactar làn-sgrìn (tùs)" +StudioProgramWindow="Proiseactar uinneagaichte (prògram)" +PreviewWindow="Proiseactar uinneagaichte (ro-shealladh)" +SceneWindow="Proiseactar uinneagaichte (sealladh)" +SourceWindow="Proiseactar uinneagaichte (tùs)" +MultiviewProjector="Ioma-shealladh (làn-sgrìn)" +MultiviewWindowed="Ioma-shealladh (uinneagaichte)" +Clear="Falamhaich" +Revert="Till" +Show="Seall" +Hide="Falaich" +UnhideAll="Seall na h-uile" +Untitled="Gun tiotal" +New="Ùr" +Duplicate="Dùblaich" +Enable="Cuir an comas" +DisableOSXVSync="Cuir à comas sioncronachadh-V air OSX" +ResetOSXVSyncOnExit="Ath-shuidhich sioncronachadh-V air OSX nuar a dh’fhàgar" +HighResourceUsage="Tha an còdachadh ro thrang! Feuch an tagh thu roghainnean video nas ìsle no an cleachd thu ro-sheata còdachaidh nas luaithe." +Transition="Tar-mhùthadh" +QuickTransitions="Tar-mhùthaidhean luatha" +Left="Clì" +Right="Deas" +Top="Barr" +Bottom="Bonn" +Reset="Ath-shuidhich" +Hours="Uair" +Minutes="Mionaid" +Seconds="Diog" +Deprecated="Cha mholar seo tuilleadh" +Import="Ion-phortaich" +Export="Às-phortaich" +Copy="Dèan lethbhreac" +Paste="Cuir ann" +PasteReference="Cuir ann (iomradh)" +PasteDuplicate="Cuir ann (dùblachadh)" +RemuxRecordings="Iompaich clàraidhean" +Next="Air adhart" +Back="Air ais" +Defaults="Bun-roghainnean" +HideMixer="Falaich sa mheasgadair" +TransitionOverride="Tar-àithneadh an tar-mhùthaidh" +None="Chan eil gin" +StudioMode.Preview="Ro-shealladh" +StudioMode.Program="Prògram" +ShowInMultiview="Seall san ioma-shealladh" +VerticalLayout="Co-dhealbhachd inghearach" +Group="Buidhnich" + +AlreadyRunning.Title="Tha OBS ’ga ruith mar-thà" +AlreadyRunning.Text="Tha OBS ’ga ruith mar-thà! Mur ann gun robh thu airson seo a dhèanamh, dùin sìos gach ionstans de dh’OBS mus fheuch thu ri ionstans eile dheth a ruith. Ma shuidhich thu OBS ach an dèid fhìor-lùghdachadh gu treidhe an t-siostaim thoir sùil a bheil e ’ga ruith an-siud fhathast." +AlreadyRunning.LaunchAnyway="Cuir gu dol e co-dhiù" + +Copy.Filters="Dèan lethbhreac dhe na criathragan" +Paste.Filters="Cuir ann criathragan" + +BandwidthTest.Region="Roinn-dùthcha" +BandwidthTest.Region.US="Na Stàitean Aonaichte" +BandwidthTest.Region.EU="An Roinn-Eòrpa" +BandwidthTest.Region.Asia="Àisia" +BandwidthTest.Region.Other="Roinn-dùthcha eile" + +Basic.FirstStartup.RunWizard="A bheil thu airson draoidh an fhèin-rèiteachaidh a ruith? ’S urrainn dhut na roghainnean agad a rèiteachadh a làimh cuideachd ’s tu a’ briogadh air a’ phutan “Roghainnean” sa phrìomh-uinneag." +Basic.FirstStartup.RunWizard.BetaWarning="(An aire: Tha draoidh an fhèin-rèiteachaidh ’na thionndadh beta fhathast)" +Basic.FirstStartup.RunWizard.NoClicked="Ma chuireas tu an caochladh romhad, ’s urrainn dhut draoidh an fhèin-rèiteachaidh a ruith a-rithist on chlàr-taice “Innealan” uair sam bith." + +Basic.AutoConfig="Draoidh an fhèin-rèiteachaidh" +Basic.AutoConfig.Beta="Draoidh an fhèin-rèiteachaidh (beta)" +Basic.AutoConfig.ApplySettings="Cuir na roghainnean an sàs" +Basic.AutoConfig.StartPage="Fiosrachadh a’ chleachdaidh" +Basic.AutoConfig.StartPage.SubTitle="Sònraich na h-adhbharan air an cleachd thu am prògram" +Basic.AutoConfig.StartPage.PrioritizeStreaming="Gleus airson sruthadh is chan eil clàradh cho cudromach sin" +Basic.AutoConfig.StartPage.PrioritizeRecording="Gleus airson clàradh a-mhàin, cha dèan mi sruthadh" +Basic.AutoConfig.VideoPage="Roghainnean video" +Basic.AutoConfig.VideoPage.SubTitle="Sònraich na roghainnean video a bu toigh leat cleachdadh" +Basic.AutoConfig.VideoPage.BaseResolution.UseCurrent="An roghainn làithreach (%1x%2)" +Basic.AutoConfig.VideoPage.BaseResolution.Display="Uidheam-taisbeanaidh %1 (%2x%3)" +Basic.AutoConfig.VideoPage.FPS.UseCurrent="An roghainn làithreach (%1)" +Basic.AutoConfig.VideoPage.FPS.PreferHighFPS="An dà chuid 60 no 30 ach b’ fhearr leam 60" +Basic.AutoConfig.VideoPage.FPS.PreferHighRes="An dà chuid 60 no 30 ach b’ fhearr leam dùmhlachd-bhreacaidh àrd" +Basic.AutoConfig.VideoPage.CanvasExplanation="An aire: Cha bhi dùmhlachd-bhreacaidh bhunasach (a’ chanabhais) co-ionnan ris an dùmhlachd-bhreacaidh a thèid a chlàradh no a shruthadh an-còmhnaidh. Dh’fhaoidte gun dèid an dùbhlachd-bhreacaidh air sruthadh no clàradh ìsleachadh o dhùmhlachd-bhreacaidh a’ chanabhais airson freagairt ri feumalachdan cleachdaidh no reat bhiotaichean." +Basic.AutoConfig.StreamPage="Fiosrachadh an t-sruthaidh" +Basic.AutoConfig.StreamPage.SubTitle="Cuid a-steach fiosrachadh an t-sruthaidh agad" +Basic.AutoConfig.StreamPage.Service="Seirbheis" +Basic.AutoConfig.StreamPage.Service.ShowAll="Seall na h-uile…" +Basic.AutoConfig.StreamPage.Server="Frithealaiche" +Basic.AutoConfig.StreamPage.StreamKey="Iuchair an t-sruthaidh" +Basic.AutoConfig.StreamPage.StreamKey.LinkToSite="(Ceangal)" +Basic.AutoConfig.StreamPage.PerformBandwidthTest="Dèan tuairmse air reat bhiotaichean le deuchainn air an leud-bhanna (dh’fhaoidte gun doir seo mionaid no dhà)" +Basic.AutoConfig.StreamPage.PreferHardwareEncoding="B’ fhearr leam còdachadh bathair-chruthaidh" +Basic.AutoConfig.StreamPage.PreferHardwareEncoding.ToolTip="Le còdachadh bathair-chruaidh, cha dèid ach glè bheag dhen CPU a chleachdadh ach dh’fhaoidte gum bi feum air reat bhiotaichean nas àirde airson ann aon ìre a chàileachd fhaighinn." +Basic.AutoConfig.StreamPage.StreamWarning.Title="Rabhadh sruthaidh" +Basic.AutoConfig.StreamPage.StreamWarning.Text="Tha an deuchainn air an leud-bhanna gu bhith dàta video tuaireamach gun fhuaim a shruthadh dhan t-seanail agad. Ma ghabhas seo a dhèanamh, mholamaid gun cuir thu dheth sàbhaladh sruthaidh video agus gun dèan thu an sruth agad prìobhaideach gus am bi an deuchainn deiseil. A bheil thu airson leantainn air adhart?" +Basic.AutoConfig.TestPage="Na toraidhean deireannach" +Basic.AutoConfig.TestPage.SubTitle.Testing="Tha am prògram a’ ruith deuchainnean airson tuairmse a dhèanamh air na roghainnean as fhearr" +Basic.AutoConfig.TestPage.SubTitle.Complete="Tha an deuchainn deiseil" +Basic.AutoConfig.TestPage.TestingBandwidth="A’ cur an leud-bhanna fo dheuchainn, dh’fhaoidte gun doir seo mionaid no dhà…" +Basic.AutoConfig.TestPage.TestingBandwidth.Connecting="A’ ceangal ri: %1…" +Basic.AutoConfig.TestPage.TestingBandwidth.ConnectFailed="Cha deach leinn ceangal a dhèanamh ri frithealaiche sam bith, thoir sùil air a’ cheangal agad dhan eadar-lìon is feuch ris a-rithist an uairsin." +Basic.AutoConfig.TestPage.TestingBandwidth.Server="A’ cur an leud-bhanna fo dheuchainn airson: %1" +Basic.AutoConfig.TestPage.TestingStreamEncoder="A’ cur inneal-còdachaidh an t-sruthaidh fo dheuchainn, dh’fhaoidte gun doir seo mionaid no dhà…" +Basic.AutoConfig.TestPage.TestingRecordingEncoder="A’ cur inneal-còdachaidh a’ chlàraidh fo dheuchainn, dh’fhaoidte gun doir seo mionaid no dhà…" +Basic.AutoConfig.TestPage.TestingRes="A’ cur nan dùmhlachdan-breacaidh fo dheuchainn, dh’fhaoidte gun doir seo mionaid no dhà…" +Basic.AutoConfig.TestPage.TestingRes.Fail="Cha deach leinn an t-inneal-còdachaidh a chur gu dol" +Basic.AutoConfig.TestPage.TestingRes.Resolution="A’ feuchainn %1x%2 %3 FPS…" +Basic.AutoConfig.TestPage.Result.StreamingEncoder="Inneal-còdachaidh an t-sruthaidh" +Basic.AutoConfig.TestPage.Result.RecordingEncoder="Inneal-còdachaidh a’ chlàraidh" +Basic.AutoConfig.TestPage.Result.Header="Rinn am prògram tuairmse gur e seo na roghainnean as fhearr dhut:" +Basic.AutoConfig.TestPage.Result.Footer="Airson na roghainnean seo a chleachdadh, briog air “Cuir na roghainnean an sàs”. Airson an draoidh a rèiteachadh às ùr is fheuchainn is a-rithist, briog air “Air ais”. Airson na roghainnean a rèiteachadh a làimh, briog air “Sguir dheth” is fosgail na “Roghainnean”." + +Basic.Stats="Stadastaireachd" +Basic.Stats.CPUUsage="Cleachdadh a’ CPU" +Basic.Stats.HDDSpaceAvailable="Àite ri fhaighinn air a’ chlàr-chruaidh" +Basic.Stats.MemoryUsage="Cleachdadh a’ chuimhne" +Basic.Stats.AverageTimeToRender="Ùine cuibheasach air reandaradh frèama" +Basic.Stats.SkippedFrames="Na frèamaichean air an leigeil seachad ri linn dàil còdachaidh" +Basic.Stats.MissedFrames="Na frèamaichean a chaidh a dhìth ri linn dàil còdachaidh" +Basic.Stats.Output.Stream="Sruthadh" +Basic.Stats.Output.Recording="Clàradh" +Basic.Stats.Status="Staid" +Basic.Stats.Status.Recording="Clàradh" +Basic.Stats.Status.Live="BEÒ" +Basic.Stats.Status.Reconnecting="’Ga ath-cheangal" +Basic.Stats.Status.Inactive="Neo-ghnìomhach" +Basic.Stats.DroppedFrames="Frèamaichean a thuit: (lìonra)" +Basic.Stats.MegabytesSent="Às-chur dàta iomlan" +Basic.Stats.Bitrate="Reat bhiotaichean" + +Updater.Title="Tha ùrachadh ri fhaighinn" +Updater.Text="Tha ùrachadh ri fhaighinn:" +Updater.UpdateNow="Ùraich an-dràsta" +Updater.RemindMeLater="Cuir nam chuimhne uaireigin eile" +Updater.Skip="Leig seachad an tionndadh" +Updater.Running.Title="Tha am prògram gnìomhach" +Updater.Running.Text="Tha às-chur gnìomhach ann, cuir stad air gach às-chur mus fheuch thu ris an ùrachadh" +Updater.NoUpdatesAvailable.Title="Chan eil ùrachadh ri fhaighinn" +Updater.NoUpdatesAvailable.Text="Chan eil ùrachadh ri fhaighinn an-dràsta" +Updater.FailedToLaunch="Cha deach leinn an t-inneal-ùrachaidh a chur gu dol" +Updater.GameCaptureActive.Title="Tha glacadh geama gnìomhach" +Updater.GameCaptureActive.Text="Tha leabharlann glacaidh geama ’ga chleachdadh. Dùin gad geama no prògram a tha ’ga ghlacadh (no ath-thòisich Windows) is feuch ris a-rithist." + +QuickTransitions.SwapScenes="Dèan iomlaid air seallaidhean an ro-sheallaidh ’s an às-chuir às dèid an tar-mhùthaidh" +QuickTransitions.SwapScenesTT="Nì seo iomlaid air seallaidhean an ro-sheallaidh ’s an às-chuir às dèidh an tar-mhùthaidh (ma tha sealladh tùsail an às-chuir ann fhathast).\nCha neo-dhèan seo atharrachadh sam bith a chaidh a dhèanamh air sealladh tùsail an às-chuir." +QuickTransitions.DuplicateScene="Dùblaich an t-sealladh" +QuickTransitions.DuplicateSceneTT="Nuair a nì thu deasachadh air an aon shealladh, leigidh seo leat cruth-atharrachadh no faicsinneachd nan tùsan a dheasachadh gun atharrachadh air an às-chur.\nAirson roghainnean nan tùsan atharrachadh gun a bhith ag atharrachadh an às-chuir, cuir an comas “Dùblaich na tùsan”.\nNuair a dh’atharraicheas tu an luach seo, thèid sealladh an às-chuir làithrich atharrachadh (ma tha e ann fhathast)." +QuickTransitions.EditProperties="Dùblaich na tùsan" +QuickTransitions.EditPropertiesTT="Nuair a nì thu deasachadh air an aon shealladh, leigidh seo leat roghainnean nan tùsan atharrachadh gun atharrachadh air an às-chur.\nCha ghabh seo a chleachdadh ach ma tha “Dùblaich an t-sealladh” an comas.\nTha tùsan ann (can tùsan glacaidh no meadhain) nach cuir taic ri seo agus cha ghabh an deasachadh fa leth.\nNuair a dh’atharraicheas tu an luach seo, thèid sealladh an às-chuir làithrich ath-shuidheachadh (ma tha e ann fhathast).\n\nRabhadh: On a thèid na tùsan a dhùblachadh, bi feum air barrachd ghoireasan siostaim no video." +QuickTransitions.HotkeyName="Tar-mhùthadh luath: %1" + +Basic.AddTransition="Cuir ris tar-mhùthadh a ghabhas rèiteachadh" +Basic.RemoveTransition="Thoir air falbh tar-mhùthadh a ghabhas rèiteachadh" +Basic.TransitionProperties="Roghainnean an tar-mhùthaidh" +Basic.SceneTransitions="Tar-mhùthaidhean an t-seallaidh" +Basic.TransitionDuration="Faide" +Basic.TogglePreviewProgramMode="Modh stiùideo" + +TransitionNameDlg.Text="Cuir a-steach ainm an tar-mhùthaidh" +TransitionNameDlg.Title="Ainm an tar-mhùthaidh" + +TitleBar.Profile="Pròifil" +TitleBar.Scenes="Seallaidhean" + +NameExists.Title="Tha an t-ainm ann mu thràth" +NameExists.Text="Tha an t-ainm seo ’ga chleachdadh mu thràth." + +NoNameEntered.Title="Cuir a-steach ainm dligheach" +NoNameEntered.Text="Chan urrainn dhut ainm falamh a chleachdadh." + +ConfirmStart.Title="A bheil thu airson tòiseachadh air sruthadh?" +ConfirmStart.Text="A bheil thu cinnteach gu bheil thu airson tòiseachadh air an t-sruthadh?" + +ConfirmStop.Title="A bheil thu airson stad a chur air an t-sruthadh?" +ConfirmStop.Text="A bheil thu cinnteach gu bheil thu airson stad a chur air an t-sruthadh?" + +ConfirmExit.Title="A bheil thu airson OBS fhàgail?" +ConfirmExit.Text="Tha OBS gnìomhach an-dràsta. Thèid gach sruthadh no clàradh a chur gu crìch. A bheil thu cinnteach gu bheil thu airson fhàgail?" + +ConfirmRemove.Title="Dearbh an toirt air falbh" +ConfirmRemove.Text="A bheil thu cinnteach gu bheil thu airson “$1” a thoirt air falbh?" +ConfirmRemove.TextMultiple="A bheil thu cinnteach gu bheil thu airson %1 nithean a thoirt air falbh?" + +Output.StartStreamFailed="Cha deach leinn tòiseachadh air an t-sruthadh" +Output.StartRecordingFailed="Cha deach leinn tòiseachadh air a’ chlàradh" +Output.StartReplayFailed="Cha deach leinn tòiseachadh air bufair na h-ath-chluiche" +Output.StartFailedGeneric="Cha deach leinn tòiseachadh air an às-chur. Thoir sùil air an loga airson barrachd fiosrachaidh.\n\nAn aire: Ma tha thu a’ cleachdadh inneal-còdachaidh NVENC no AMD, dèan cinneach gu bheil na draibhearan video agad cho ùr ’s a ghabhas." + +Output.ConnectFail.Title="Cha deach leinn ceangal a dhèanamh" +Output.ConnectFail.BadPath="Tha slighe no URL a’ cheangail mì-dhligheach. Thoir sùil air na roghainnean agad feuch a bheil iad mar bu chòir." +Output.ConnectFail.ConnectFailed="Dh’fhàillig an ceangal ris an fhrithealaiche" +Output.ConnectFail.InvalidStream="Bha b’ urrainn dhuinn an t-seanail no an iuchair sruthaidh a shònraich thu inntrigeadh, dearbhaidh an iuchair sruthaidh agad. Ma tha i ceart, dh’fhaoidte gu bheil duilgheadas ann le ceangal ris an fhrithealaiche." +Output.ConnectFail.Error="Thachair mearachd ris nach robh dùil nuair a dh’fheuch sinn ri ceangal ris an fhrithealaiche. Tha barrachd fiosrachaidh ann am faidhle an loga." +Output.ConnectFail.Disconnected="Chaidh an ceangal dhan fhrithealaiche a bhriseadh." + +Output.RecordFail.Title="Cha deach leinn tòiseachadh air a’ chlàradh" +Output.RecordFail.Unsupported="Cha chuirear taic ri fòrmat an às-chuir no cha chuir e taic ri corr is aon traca fuaime. Thoir sùil air na roghainnean agad is feuch ris a-rithist." +Output.RecordNoSpace.Title="Chan eil àite gu leòr air an diosga" +Output.RecordNoSpace.Msg="Chan eil àite gu leòr air an diosga airson leantainn air adhart leis a’ chlàradh." +Output.RecordError.Title="Mearachd clàraidh" +Output.RecordError.Msg="Thachair mearachd nach deach a shònrachadh rè a’ chlàraidh." +Output.ReplayBuffer.NoHotkey.Title="Cha deach grad-iuchair a shuidheachadh!" +Output.ReplayBuffer.NoHotkey.Msg="Cha deach grad-iuchair sàbhalaidh a shuidheachadh airson bufair na h-ath-chluiche. Suidhich an grad-iuchair “Sàbhail” a thèid a chleachdadh airson clàraidhean air ath-chluichean a shàbhaladh." + +Output.BadPath.Title="Droch-shlighe faidhle" +Output.BadPath.Text="Chan eil an t-slighe às-chuir a chaidh a rèiteachadh dligheach. Thoir sùil air na roghainnean agad airson dearbhadh gun deach slighe faidhle dhligheach a shuidheachadh." + +LogReturnDialog="Chaidh an loga a luchdadh suas" +LogReturnDialog.CopyURL="Dèan lethbhreac dhen URL" +LogReturnDialog.ErrorUploadingLog="Mearachd le luchdadh suas an loga" + +LicenseAgreement="Aonta ceadachais" +LicenseAgreement.PleaseReview="Thoir sùil air teirmichean a’ cheadachais mus cleachd thu OBS. Le cleachdadh a’ phrògraim seo, aidichidh tu gun do leugh thu is gun aontaich thu ri teirmichean ceadachas GNU General Public License v2.0. Sgrolaich sìos airson an corr dhen aonta fhaicinn." +LicenseAgreement.ClickIAgreeToContinue="Ma ghabhas tu ri teirmichean an aonta, briog air “Gabhaidh mi ris” airson leantainn air adhart. Feumaidh tu gabhail ris an aonta airson OBS a chleachdadh." +LicenseAgreement.IAgree="Gabhaidh mi ris" +LicenseAgreement.Exit="Fàg an-seo" + +Remux.SourceFile="Clàradh OBS" +Remux.TargetFile="Faidhle amais" +Remux.Remux="Iompaich" +Remux.OBSRecording="Clàradh OBS" +Remux.FinishedTitle="Tha an t-iompachadh deiseil" +Remux.Finished="Chaidh an clàradh iompachadh" +Remux.FinishedError="Chaidh an clàradh iompachadh ach ’s ma dh’fhaoidte nach eil am faidhle coileanta" +Remux.SelectRecording="Tagh clàradh OBS…" +Remux.SelectTarget="Tagh faidhle amais…" +Remux.FileExistsTitle="Tha am faidhle amais ann" +Remux.FileExists="Tha am faidhle amais ann mu thràth, a bheil thu airson seo a chur ’na àite?" +Remux.ExitUnfinishedTitle="’Ga iompachadh" +Remux.ExitUnfinished="Chan eil an t-iompachadh deiseil agus dh’fhaoidte nach gabh am faidhle amais a cleachdadh ma chuireas tu stad air an-dràsta.\nA bheil thu cinnteach gu bheil thu airson stad a chur air an iompachadh?" + +UpdateAvailable="Tha ùrachadh ri fhaighinn" +UpdateAvailable.Text="Tha tionndadh %1.%2.%3 ri fhaighinn a-nis. Briog an-seo gus a luchdadh a-nuas" + +Basic.DesktopDevice1="Fuaim an deasg" +Basic.DesktopDevice2="Fuaim an deasg 2" +Basic.AuxDevice1="Micreofon/Taic" +Basic.AuxDevice2="Micreofon/Taic 2" +Basic.AuxDevice3="Micreofon/Taic 3" +Basic.AuxDevice4="Micreofon/Taic 4" + +Basic.Scene="Sealladh" +Basic.DisplayCapture="Glacadh an uidheim-taisbeanaidh" + +Basic.Main.PreviewConextMenu.Enable="Cuir an ro-shealladh an comas" + +ScaleFiltering="Criathradh sgèilidh" +ScaleFiltering.Point="Puing" +ScaleFiltering.Bilinear="Dà-loidhneach" +ScaleFiltering.Bicubic="Dà-chiùbach" +ScaleFiltering.Lanczos="Lanczos" + +Deinterlacing="Dì-fhilleadh" +Deinterlacing.Discard="Tilg air falbh" +Deinterlacing.Retro="Retro" +Deinterlacing.Blend="Co-mheasgachadh" +Deinterlacing.Blend2x="Co-mheasgachadh 2x" +Deinterlacing.Linear="Loidhneach" +Deinterlacing.Linear2x="Loidhneach 2x" +Deinterlacing.Yadif="Yadif" +Deinterlacing.Yadif2x="Yadif 2x" +Deinterlacing.TopFieldFirst="An raon air a’ bharr an toiseach" +Deinterlacing.BottomFieldFirst="An raon aig a’ bhonn an toiseach" + +VolControl.SliderUnmuted="Sleamhnachan àirde airson “%1”: %2" +VolControl.SliderMuted="Sleamhnachan àirde airson “%1”: %2 (mùchte an-dràsta)" +VolControl.Mute="Mùch “%1”" +VolControl.Properties="Roghainnean airson “%1”" + +Basic.Main.AddSceneDlg.Title="Cuir sealladh ris" +Basic.Main.AddSceneDlg.Text="Cuir a-steach ainm an t-seallaidh" + +Basic.Main.DefaultSceneName.Text="Sealladh %1" + +Basic.Main.AddSceneCollection.Title="Cuir cruinneachadh sheallaidhean ris" +Basic.Main.AddSceneCollection.Text="Cuir a-steach ainm a’ chruinneachaidh sheallaidhean" + +Basic.Main.RenameSceneCollection.Title="Thoir ainm ùr air a’ chruinneachadh sheallaidhean" + +AddProfile.Title="Cuir pròifil ris" +AddProfile.Text="Cuir a-steach ainm na pròifil" + +RenameProfile.Title="Thoir ainm ùr air a’ phròifil" + +Basic.Main.MixerRename.Title="Thoir ainm ùr air an tùs fuaime" +Basic.Main.MixerRename.Text="Cuir a-steach ainm an tùis fuaime" + + +Basic.Main.PreviewDisabled="Tha an ro-shealladh às comas an-dràsta" + +Basic.SourceSelect="Cruthaich/Tagh tùs" +Basic.SourceSelect.CreateNew="Cruthaich tùs ùr" +Basic.SourceSelect.AddExisting="Cuir ris tùs a tha ann" +Basic.SourceSelect.AddVisible="Seall an tùs" + +Basic.PropertiesWindow="Roghainnean airson “%1”" +Basic.PropertiesWindow.AutoSelectFormat="%1 (taghadh fèin-obrachail: %2)" +Basic.PropertiesWindow.SelectColor="Tagh dath" +Basic.PropertiesWindow.SelectFont="Tagh cruth-clò" +Basic.PropertiesWindow.ConfirmTitle="Chaidh na roghainnean atharrachadh" +Basic.PropertiesWindow.Confirm="Tha atharraichean gun sàbhaladh ann. A bheil thu airson an cumail?" +Basic.PropertiesWindow.NoProperties="Chan eil roghainn ann" +Basic.PropertiesWindow.AddFiles="Cuir faidhlichean ris" +Basic.PropertiesWindow.AddDir="Cuir pasgan ris" +Basic.PropertiesWindow.AddURL="Cuir slighe/URL ris" +Basic.PropertiesWindow.AddEditableListDir="Cuir pasgan ri “%1”" +Basic.PropertiesWindow.AddEditableListFiles="Cuir faidhlichean ri “%1”" +Basic.PropertiesWindow.AddEditableListEntry="Cuir innteart ri “%1”" +Basic.PropertiesWindow.EditEditableListEntry="Deasaich innteart o “%1”" + +Basic.PropertiesView.FPS.Simple="Luachan FPS simplidh" +Basic.PropertiesView.FPS.Rational="Luachan FPS reusanta" +Basic.PropertiesView.FPS.ValidFPSRanges="Rainsean FPS dligheach:" + +Basic.InteractionWindow="Conaltradh le “%1”" + +Basic.StatusBar.Reconnecting="Gun cheangal, ’ga ath-cheangal an ceann %2 diog(an) (oidhirp %1)" +Basic.StatusBar.AttemptingReconnect="A’ feuchainn ri ath-cheangal… (oidhirp %1)" +Basic.StatusBar.ReconnectSuccessful="Chaidh ath-cheangal" +Basic.StatusBar.Delay="Dàil (%1d)" +Basic.StatusBar.DelayStartingIn="Dàil (a’ tòiseachadh an ceann %1d)" +Basic.StatusBar.DelayStoppingIn="Dàil (a’ stad an ceann %1d)" +Basic.StatusBar.DelayStartingStoppingIn="Dàil (a’ stad an ceann %1d, a’ tòiseachadh an ceann %1d)" + +Basic.Filters="Criathragan" +Basic.Filters.AsyncFilters="Criathragan fuaime/video" +Basic.Filters.AudioFilters="Criathragan fuaime" +Basic.Filters.EffectFilters="Criathragan èifeachd" +Basic.Filters.Title="Crathragan airson “%1”" +Basic.Filters.AddFilter.Title="Ainm na criathraige" +Basic.Filters.AddFilter.Text="Cuir a-steach ainm na criathraige" + +Basic.TransformWindow="Tar-mhùthadh air nì an t-seallaidh" +Basic.TransformWindow.Position="Ionad" +Basic.TransformWindow.Rotation="Cuairteachadh" +Basic.TransformWindow.Size="Meud" +Basic.TransformWindow.Alignment="Co-thaobhadh ionaid" +Basic.TransformWindow.BoundsType="Seòrsa a’ bhogsa-iadhaidh" +Basic.TransformWindow.BoundsAlignment="Co-thaobhadh sa bhogsa-iadhaidh" +Basic.TransformWindow.Bounds="Meud a’ bhogsa-iadhaidh" +Basic.TransformWindow.Crop="Bearr" + +Basic.TransformWindow.Alignment.TopLeft="Taobh clì aig a’ bharr" +Basic.TransformWindow.Alignment.TopCenter="Sa mheadhan aig a’ bharr" +Basic.TransformWindow.Alignment.TopRight="Taobh deas aig a’ bharr" +Basic.TransformWindow.Alignment.CenterLeft="Sa mheadhan air an taobh chlì" +Basic.TransformWindow.Alignment.Center="Sa mheadhan" +Basic.TransformWindow.Alignment.CenterRight="Sa mheadhan air an taobh deas" +Basic.TransformWindow.Alignment.BottomLeft="Taobh clì aig a’ bhonn" +Basic.TransformWindow.Alignment.BottomCenter="Sa mheadhan aig a’ bhonn" +Basic.TransformWindow.Alignment.BottomRight="Taobh deas aig a’ bhonn" + +Basic.TransformWindow.BoundsType.None="Gun iadhadh" +Basic.TransformWindow.BoundsType.MaxOnly="Am meud as motha a-mhàin" +Basic.TransformWindow.BoundsType.ScaleInner="Sgèilich ri iadhadh a-staigh" +Basic.TransformWindow.BoundsType.ScaleOuter="Sgèilich ri iadhadh a-muigh" +Basic.TransformWindow.BoundsType.ScaleToWidth="Sgèilich ri leud an iadhaidh" +Basic.TransformWindow.BoundsType.ScaleToHeight="Sgèilich ri àirde an iadhaidh" +Basic.TransformWindow.BoundsType.Stretch="Sìn gun iadhadh" + +Basic.Main.AddSourceHelp.Title="Chan urrainn dhuinn an tùs a chur ris" +Basic.Main.AddSourceHelp.Text="Feumaidh do shealladh a bhith agad mus cuir thu tùs ris." + +Basic.Main.Scenes="Seallaidhean" +Basic.Main.Sources="Tùsan" +Basic.Main.Controls="Uidheaman-smachd" +Basic.Main.Connecting="’Ga cheangal…" +Basic.Main.StartRecording="Tòisich air clàradh" +Basic.Main.StartReplayBuffer="Tòisich air bufair ath-chluiche" +Basic.Main.StartStreaming="Tòisich air sruthadh" +Basic.Main.StopRecording="Cuir stad air a’ chlàradh" +Basic.Main.StoppingRecording="A’ cur stad air a’ chlàradh…" +Basic.Main.StopReplayBuffer="Cuir stad air bufair na h-ath-chluiche" +Basic.Main.StoppingReplayBuffer="A’ cur stad air bufair na h-ath-chluiche…" +Basic.Main.StopStreaming="Cuir stad air an t-sruthadh" +Basic.Main.StoppingStreaming="A’ cur stad air an t-sruthadh…" +Basic.Main.ForceStopStreaming="Cuir stad air an t-sruthadh (leig seachad an dàil)" +Basic.Main.Group="Buidheann %1" +Basic.Main.GroupItems="Buidhnich na thagh thu" +Basic.Main.Ungroup="Sgaoil am buidheann" + +Basic.MainMenu.File="&Faidhle" +Basic.MainMenu.File.Export="Às-phor&taich" +Basic.MainMenu.File.Import="&Ion-phortaich" +Basic.MainMenu.File.ShowRecordings="Seall na &clàraidhean" +Basic.MainMenu.File.Remux="Io&mpaich na clàraidhean" +Basic.MainMenu.File.Settings="&Roghainnean" +Basic.MainMenu.File.ShowSettingsFolder="Seall pasgan nan roghainnean" +Basic.MainMenu.File.ShowProfileFolder="Seall pasgan na pròifil" +Basic.MainMenu.AlwaysOnTop="&Air uachdar an-còmhnaidh" +Basic.MainMenu.File.Exit="&Fàg an-seo" + +Basic.MainMenu.Edit="D&easaich" +Basic.MainMenu.Edit.Undo="&Neo-dhèan" +Basic.MainMenu.Edit.Redo="Ath-&dhèan" +Basic.MainMenu.Edit.UndoAction="&Neo-dhèan $1" +Basic.MainMenu.Edit.RedoAction="Ath-&dhèan $1" +Basic.MainMenu.Edit.LockPreview="G&lais an ro-shealladh" +Basic.MainMenu.Edit.Scale="&Sgèileadh an ro-sheallaidh" +Basic.MainMenu.Edit.Scale.Window="Sgèilich ris an uinneag" +Basic.MainMenu.Edit.Scale.Canvas="Canabhas (%1x%2)" +Basic.MainMenu.Edit.Scale.Output="Às-chur (%1x%2)" +Basic.MainMenu.Edit.Transform="&Tar-mhùth" +Basic.MainMenu.Edit.Transform.EditTransform="D&easaich an tar-mhùthadh…" +Basic.MainMenu.Edit.Transform.CopyTransform="Dèan lethbhreac dhen tar-mhùthadh" +Basic.MainMenu.Edit.Transform.PasteTransform="Cuir ann tar-mhùthadh" +Basic.MainMenu.Edit.Transform.ResetTransform="Ath-&shuidhich an tar-mhùthadh" +Basic.MainMenu.Edit.Transform.Rotate90CW="Cuairtich gu deiseil le 90 ceum" +Basic.MainMenu.Edit.Transform.Rotate90CCW="Cuairtich gu tuathail le 90 ceum" +Basic.MainMenu.Edit.Transform.Rotate180="Cuairtich le 180 ceum" +Basic.MainMenu.Edit.Transform.FlipHorizontal="T&hoir flip air a’ chòmhnard" +Basic.MainMenu.Edit.Transform.FlipVertical="&Thoir flip gu h-inghearach" +Basic.MainMenu.Edit.Transform.FitToScreen="Co-&fhreagair ri meud na sgrìn" +Basic.MainMenu.Edit.Transform.StretchToScreen="&Sìn gu meud na sgrìn" +Basic.MainMenu.Edit.Transform.CenterToScreen="Cuir air &meadhan na sgrìn" +Basic.MainMenu.Edit.Order="Òrdu&gh" +Basic.MainMenu.Edit.Order.MoveUp="Gluais s&uas" +Basic.MainMenu.Edit.Order.MoveDown="Gluais &sìos" +Basic.MainMenu.Edit.Order.MoveToTop="Gluais gun bh&arr" +Basic.MainMenu.Edit.Order.MoveToBottom="Gluais gun &bhonn" +Basic.MainMenu.Edit.AdvAudio="Roghainnean &adhartach na fuaime" + +Basic.MainMenu.View="&Seall" +Basic.MainMenu.View.Toolbars="&Bàraichean-inneal" +Basic.MainMenu.View.Docks="Docaichean" +Basic.MainMenu.View.Docks.ResetUI="Ath-shuidhich an eadar-aghaidh" +Basic.MainMenu.View.Docks.LockUI="Glais an eadar-aghaidh" +Basic.MainMenu.View.Toolbars.Listboxes="Bogsaichean-&liosta" +Basic.MainMenu.View.SceneTransitions="Tar-&mhùthaidhean an t-seallaidh" +Basic.MainMenu.View.StatusBar="Bàr-s&taide" +Basic.MainMenu.View.Fullscreen.Interface="Eadar-aghaidh làn-sgrìn" + +Basic.MainMenu.SceneCollection="Cruinneachadh &sheallaidhean" +Basic.MainMenu.Profile="&Pròifil" +Basic.MainMenu.Profile.Import="Ion-phortaich pròifil" +Basic.MainMenu.Profile.Export="Às-phortaich a’ phròifil" +Basic.MainMenu.SceneCollection.Import="Ion-phortaich cruinneachadh sheallaidhean" +Basic.MainMenu.SceneCollection.Export="Às-phortaich cruinneachadh sheallaidhean" +Basic.MainMenu.Profile.Exists="Tha a’ phròifil ann mu thràth" +Basic.MainMenu.SceneCollection.Exists="Tha an cruinneachadh sheallaidhean ann mu thràth" + +Basic.MainMenu.Tools="Innea&lan" + +Basic.MainMenu.Help="Cob&hair" +Basic.MainMenu.Help.HelpPortal="&Portal na cobharach" +Basic.MainMenu.Help.Website="&Tadhail air an làrach-lìn" +Basic.MainMenu.Help.Logs="Faidhlichean an &loga" +Basic.MainMenu.Help.Logs.ShowLogs="&Seall faidhlichean an loga" +Basic.MainMenu.Help.Logs.UploadCurrentLog="Luchdai&ch suas faidhle an loga làithrich" +Basic.MainMenu.Help.Logs.UploadLastLog="&Luchdaich suas faidhle an loga mu dheireadh" +Basic.MainMenu.Help.Logs.ViewCurrentLog="&Seall an loga làithreach" +Basic.MainMenu.Help.CheckForUpdates="Thoir sùil airson ùrachaidhean" +Basic.MainMenu.Help.CrashLogs="Aithis&gean tuislidh" +Basic.MainMenu.Help.CrashLogs.ShowLogs="&Seall na h-aithisgean tuislidh" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="&Luchdaich suas an aithisg tuislidh mu dheireadh" + +Basic.Settings.ProgramRestart="Feumaidh tu am prògram ath-thòiseachadh gus na roghainnean seo a chur an sàs." +Basic.Settings.ConfirmTitle="Dearbh na h-atharraichean" +Basic.Settings.Confirm="Tha atharraichean gun sàbhaladh agad. A bheil thu airson an sàbhaladh?" + +Basic.Settings.General="Coitcheann" +Basic.Settings.General.Theme="Ùrlar" +Basic.Settings.General.Language="Cànan" +Basic.Settings.General.EnableAutoUpdates="Thoir sùil airson ùrachaidhean gu fèin-obrachail aig an toiseach" +Basic.Settings.General.OpenStatsOnStartup="Fosglaidh seo còmhradh na stadastaireachd aig an toiseach" +Basic.Settings.General.WarnBeforeStartingStream="Seall còmhradh dearbhaidh mus dèid sruthadh a thòiseachadh" +Basic.Settings.General.WarnBeforeStoppingStream="Seall còmhradh dearbhaidh mus dèid stad a chur air sruthadh" +Basic.Settings.General.Projectors="Proiseactaran" +Basic.Settings.General.HideProjectorCursor="Falaich an cùrsair os cionn proiseactaran" +Basic.Settings.General.ProjectorAlwaysOnTop="Cuir na proiseactaran air uachdar an-còmhnaidh" +Basic.Settings.General.Snapping="Greimeachadh co-thaobhadh nan tùsan" +Basic.Settings.General.ScreenSnapping="Greimich na tùsan ri oir na sgrìn" +Basic.Settings.General.CenterSnapping="Greimich na tùsan ris a’ mheadhan" +Basic.Settings.General.SourceSnapping="Greimich na tùsan ri tùsan eile" +Basic.Settings.General.SnapDistance="Mothalachd a’ ghreimeachaidh" +Basic.Settings.General.RecordWhenStreaming="Clàraich gu fèil-obrachail nuair a thèid rud a shruthadh" +Basic.Settings.General.KeepRecordingWhenStreamStops="Cum an clàradh nuair a thèid stad a chur air an t-sruthadh" +Basic.Settings.General.ReplayBufferWhileStreaming="Tòisich bufair na h-ath-chluiche gu fèin-obrachail nuair a bhios rud ’ga shruthadh" +Basic.Settings.General.KeepReplayBufferStreamStops="Cum bufair na h-ath-chluiche gnìomhach nuair a thèid stad a chur air sruthadh" +Basic.Settings.General.SysTray="Treidhe an t-siostaim" +Basic.Settings.General.SysTrayWhenStarted="Fìor-lùghdaich gu treidhe an t-siostaim aig an toiseach" +Basic.Settings.General.SystemTrayHideMinimize="Fìor-lùghdaich gu treidhe an t-siostaim seach bàr nan saothair an-còmhnaidh" +Basic.Settings.General.SaveProjectors="Sàbhail na proiseactaran nuair a thèid fàgail an-seo" +Basic.Settings.General.SwitchOnDoubleClick="Tar-mhùth gu sealladh le briogadh dùbailte" +Basic.Settings.General.StudioPortraitLayout="Cuir an comas co-dhealbhachd portraid/inghearach" +Basic.Settings.General.Multiview="Ioma-shealladh" +Basic.Settings.General.Multiview.MouseSwitch="Briog airson leum a ghearradh eadar seallaidhean" +Basic.Settings.General.Multiview.DrawSourceNames="Seall ainmean nan seallaidhean" +Basic.Settings.General.Multiview.DrawSafeAreas="Tarraing raointean sàbhailte (EBU R 95)" +Basic.Settings.General.MultiviewLayout="Co-dhealbhachd an ioma-sheallaidh" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Còmhnard, barr (8 seallaidhean)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Còmhnard, bonn (8 seallaidhean)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Inghearach, clì (8 seallaidhean)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Inghearach, deas (8 seallaidhean)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Còmhnard, barr (24 sealladh)" + +Basic.Settings.Stream="Sruthadh" +Basic.Settings.Stream.StreamType="Seòrsa an t-sruthaidh" + +Basic.Settings.Output="Às-chur" +Basic.Settings.Output.Format="Fòrmat a’ chlàraidh" +Basic.Settings.Output.Encoder="Inneal-còdachaidh" +Basic.Settings.Output.SelectDirectory="Tagh pasgan a’ chlàraidh" +Basic.Settings.Output.SelectFile="Tagh faidhle clàraidh" +Basic.Settings.Output.EnforceBitrate="Èignich cuingeachaidhean reat bhiotaichean aig an t-seirbheis sruthaidh" +Basic.Settings.Output.Mode="Modh an às-chuir" +Basic.Settings.Output.Mode.Simple="Simplidh" +Basic.Settings.Output.Mode.Adv="Adhartach" +Basic.Settings.Output.Mode.FFmpeg="Às-chur FFmpeg" +Basic.Settings.Output.UseReplayBuffer="Cuir an comas bufair ath-chluiche" +Basic.Settings.Output.ReplayBuffer.SecondsMax="Ùine as motha nan ath-chluichean (diog)" +Basic.Settings.Output.ReplayBuffer.MegabytesMax="A’ chuimhne as motha (meaga-baidht)" +Basic.Settings.Output.ReplayBuffer.Estimate="Tuairmse air cleachdadh na cuimhne: %1 MB" +Basic.Settings.Output.ReplayBuffer.EstimateUnknown="Chan urrainn dhuinn tuairmse a dhèanamh air cleachdadh na cuimhne. Suidhich crìoch as motha na cuimhne." +Basic.Settings.Output.ReplayBuffer.HotkeyMessage="(An aire: Dèan cinnteach gun suidhich thu grad-iuchair airson bufair na h-ath-chluiche ann an earrann nan grad-iuchraichean)" +Basic.Settings.Output.ReplayBuffer.Prefix="Ro-leasachan air ainmean faidhle aig bufair na h-ath-chluiche" +Basic.Settings.Output.ReplayBuffer.Suffix="Iar-leasachan" +Basic.Settings.Output.Simple.SavePath="Slighe a’ chlàraidh" +Basic.Settings.Output.Simple.RecordingQuality="Càileachd a’ chlàraidh" +Basic.Settings.Output.Simple.RecordingQuality.Stream="Co-ionnann ri tè an t-sruthaidh" +Basic.Settings.Output.Simple.RecordingQuality.Small="Càileachd àrd, faidhle meadhanach mòr" +Basic.Settings.Output.Simple.RecordingQuality.HQ="Cha ghabh diofar aithneachadh, faidhle mòr" +Basic.Settings.Output.Simple.RecordingQuality.Lossless="Càileachd gun chall, faidhle uabhasach mòr" +Basic.Settings.Output.Simple.Warn.VideoBitrate="Rabhadh: Thèid reat bhiotaichean video an t-sruthaidh a shuidheachadh air %1, seo a’ chrìoch as àirde aig an t-seirbheis sruthaidh làithreach. Ma tha thu cinnteach bu bheil thu airson corr is %1 a chleachdadh, cuir an comas roghainnean adhartach an inneil-chòdachaidh agus thoir a’ chromag far “Èignich cuingeachaidhean reat bhiotaichean aig an t-seirbheis sruthaidh”." +Basic.Settings.Output.Simple.Warn.AudioBitrate="Rabhadh: Thèid reat bhiotaichean fuaime an t-sruthaidh a shuidheachadh air %1, seo a’ chrìoch as àirde aig an t-seirbheis sruthaidh làithreach. Ma tha thu cinnteach bu bheil thu airson corr is %1 a chleachdadh, cuir an comas roghainnean adhartach an inneil-chòdachaidh agus thoir a’ chromag far “Èignich cuingeachaidhean reat bhiotaichean aig an t-seirbheis sruthaidh”." +Basic.Settings.Output.Simple.Warn.Encoder="Rabhadh: Ma nì thu clàradh le inneal-còdachaidh bathair-bhog air nach eil an aon chàileachd ’s a th’ air an t-sruthadh, bi feum air barrachd cleachdadh a’ CPU nuair a bhios tu a’ sruthadh ’s a’ clàradh aig an aon àm." +Basic.Settings.Output.Simple.Warn.Lossless="Rabhadh: Cruthaichidh càileachd gun chall faidhlichean uabhasach mòr! Faodaidh càileachd gun chall corr is 7 giga-baidht a dh’àite a chleachdadh air an diosg gach mionaid ma tha an dùmhlachd-bhreacaidh agus an reat fhrèamaichean àrd. Cha mholamaid càileachd gun chall airson clàraidhean fada ach ma tha torr àite agad air an diosg." +Basic.Settings.Output.Simple.Warn.Lossless.Msg="A bheil thu cinnteach gu bheil thu airson càileachd gun chall a chleachdadh?" +Basic.Settings.Output.Simple.Warn.Lossless.Title="Rabhadh a thaobh càileachd gun chall!" +Basic.Settings.Output.Simple.Warn.MultipleQSV="Rabhadh: Chan urrainn dhut iomadh inneal-còdachaidh QSV fa leth a chleachdadh nuair a bhios tu a’ sruthadh ’s a clàradh aig an aon àm. Nam bu toigh leat sruthadh is clàradh aig an aon àm, feuch an atharraich thu inneal-còdachaidh a’ chlàraidh no an t-sruthaidh." +Basic.Settings.Output.Simple.Encoder.Software="Bathar-bog (x264)" +Basic.Settings.Output.Simple.Encoder.Hardware.QSV="Bathar-cruaidh (QSV)" +Basic.Settings.Output.Simple.Encoder.Hardware.AMD="Bathar-cruaidh (AMD)" +Basic.Settings.Output.Simple.Encoder.Hardware.NVENC="Bathar-cruaidh (NVENC)" +Basic.Settings.Output.Simple.Encoder.SoftwareLowCPU="Bathar-bog (ro-shuidheachadh air x264 le cleachdadh a’ CPU ìosal, meudaichidh seo na faidhlichean)" +Basic.Settings.Output.VideoBitrate="Reat bhiotaichean a’ video" +Basic.Settings.Output.AudioBitrate="Reat bhiotaichean na fuaime" +Basic.Settings.Output.Reconnect="Ath-cheangail gu fèin-obrachail" +Basic.Settings.Output.RetryDelay="Dàil na feuchainn a-rithist (diog)" +Basic.Settings.Output.MaxRetries="Oidhirpean as motha" +Basic.Settings.Output.Advanced="Cuir an comas roghainnean adhartach an inneil-chòdachaidh" +Basic.Settings.Output.EncoderPreset="Ro-shuidheachadh an inneil-chòdachaidh (nas àirde = nas lugha dhen CPU)" +Basic.Settings.Output.CustomEncoderSettings="Roghainnean gnàthaichte an inneil-chòdachaidh" +Basic.Settings.Output.CustomMuxerSettings="Roghainnean gnàthaichte an iompaicheir" +Basic.Settings.Output.NoSpaceFileName="Gin ainm faidhle gun spàs" + +Basic.Settings.Output.Adv.Rescale="Ath-sgèilich an t-às-chur" +Basic.Settings.Output.Adv.AudioTrack="Traca fuaime" +Basic.Settings.Output.Adv.Streaming="Sruthadh" +Basic.Settings.Output.Adv.ApplyServiceSettings="Èignich na roghainnean inneil-chòdachaidh aig an t-seirbheis sruthaidh" +Basic.Settings.Output.Adv.Audio.Track1="Traca 1" +Basic.Settings.Output.Adv.Audio.Track2="Traca 2" +Basic.Settings.Output.Adv.Audio.Track3="Traca 3" +Basic.Settings.Output.Adv.Audio.Track4="Traca 4" +Basic.Settings.Output.Adv.Audio.Track5="Traca 5" +Basic.Settings.Output.Adv.Audio.Track6="Traca 6" + +Basic.Settings.Output.Adv.Recording="Clàradh" +Basic.Settings.Output.Adv.Recording.Type="Seòrsa" +Basic.Settings.Output.Adv.Recording.Type.Standard="Stannardach" +Basic.Settings.Output.Adv.Recording.Type.FFmpegOutput="Às-chur gnàthaichte (FFmpeg)" +Basic.Settings.Output.Adv.Recording.UseStreamEncoder="(Cleachd inneal-còdachaidh sruthaidh)" +Basic.Settings.Output.Adv.Recording.Filename="Fòrmatadh nan ainmean faidhle" +Basic.Settings.Output.Adv.Recording.OverwriteIfExists="Sgrìobh thairis air faidhle ma tha e ann" +Basic.Settings.Output.Adv.FFmpeg.Type="Seòrsa às-chur FFmpeg" +Basic.Settings.Output.Adv.FFmpeg.Type.URL="Às-chuir gu URL" +Basic.Settings.Output.Adv.FFmpeg.Type.RecordToFile="Às-chuir gu faidhle" +Basic.Settings.Output.Adv.FFmpeg.SaveFilter.Common="Fòrmatan clàraidh cumanta" +Basic.Settings.Output.Adv.FFmpeg.SaveFilter.All="Na h-uile faidhle" +Basic.Settings.Output.Adv.FFmpeg.SavePathURL="Slighe faidhle no URL" +Basic.Settings.Output.Adv.FFmpeg.Format="Fòrmat an t-soithich" +Basic.Settings.Output.Adv.FFmpeg.FormatAudio="Fuaim" +Basic.Settings.Output.Adv.FFmpeg.FormatVideo="Video" +Basic.Settings.Output.Adv.FFmpeg.FormatDefault="Am fòrmat tùsail" +Basic.Settings.Output.Adv.FFmpeg.FormatDesc="Tuairisgeul air fòrmat an t-soithich" +Basic.Settings.Output.Adv.FFmpeg.FormatDescDef="Chaidh codec fuaime/video a thuairmeas o shlighe an fhaidhle no URL" +Basic.Settings.Output.Adv.FFmpeg.AVEncoderDefault="An t-inneal-còdachaidh tùsail" +Basic.Settings.Output.Adv.FFmpeg.AVEncoderDisable="Cuir an t-inneal-còdachaidh à comas" +Basic.Settings.Output.Adv.FFmpeg.VEncoder="Inneal-còdachaidh video" +Basic.Settings.Output.Adv.FFmpeg.VEncoderSettings="Roghainnean an inneil-chòdachaidh video (ma tha gin ann)" +Basic.Settings.Output.Adv.FFmpeg.AEncoder="Inneal-còdachaidh fuaime" +Basic.Settings.Output.Adv.FFmpeg.AEncoderSettings="Roghainnean an inneil-chòdachaidh fuaime (ma tha gin ann)" +Basic.Settings.Output.Adv.FFmpeg.MuxerSettings="Roghainnean an iompaicheir (ma tha gin ann)" +Basic.Settings.Output.Adv.FFmpeg.GOPSize="Eadaramh nam frèamaichean-iuchrach (frèam)" +Basic.Settings.Output.Adv.FFmpeg.IgnoreCodecCompat="Seall na h-uile codec (fiù an fheadhainn nach eil co-chòrdail ma dh’fhaoidte)" + +FilenameFormatting.completer="%CCYY-%MM-%DD %hh-%mm-%ss\n%YY-%MM-%DD %hh-%mm-%ss\n%Y-%m-%d %H-%M-%S\n%y-%m-%d %H-%M-%S\n%a %Y-%m-%d %H-%M-%S\n%A %Y-%m-%d %H-%M-%S\n%Y-%b-%d %H-%M-%S\n%Y-%B-%d %H-%M-%S\n%Y-%m-%d %I-%M-%S-%p\n%Y-%m-%d %H-%M-%S-%z\n%Y-%m-%d %H-%M-%S-%Z" + +FilenameFormatting.TT="%CCYY Am bliadhna, ceithir àireamhan\n%YY Am bliadhna, an dà àireamh mu dheireadh (00-99)\n%MM Am mìos ’na àireamh dheicheach (01-12)\n%DD Latha a’ mhìosa, le pada neoni (01-31)\n%hh An uair san fhòrmat 24u (00-23)\n%mm Am mionaid (00-59)\n%ss An diog (00-61)\n%% Samhla %\n%a Gearr-ainm air latha na seachdaine\n%A Ainm slàn air latha na seachdaine\n%b Gearr-ainm a’ mhìosa\n%B Ainm slàn a’ mhìosa\n%d Latha a’ mhìosa le pada neoni (01-31)\n%H An uair san fhòrmat 24u (00-23)\n%I An uair san fhòrmat 12u (01-12)\n%m Am mìos ’na àireamh dheicheach (01-12)\n%M A’ mhionaid (00-59)\n%p Sònrachadh m no f\n%S An diog (00-61)\n%y am bliadhna, an dà àireamh mu dheireadh (00-99)\n%Y Am bliadhna\n%z Frìth-àireamh ISO 8601 o UTC no\n ainm no giorrachadh na roinn-tìde\n%Z Ainm no giorrachadh na roinn-tìde\n" + +Basic.Settings.Video="Video" +Basic.Settings.Video.Adapter="Adaptar video" +Basic.Settings.Video.BaseResolution="Dùmhlachd-bhreacaidh bhunasach (a’ chanabhais)" +Basic.Settings.Video.ScaledResolution="Dùmhlachd-bhreacaidh (sgèilichte) an às-chuir" +Basic.Settings.Video.DownscaleFilter="Criathrag sgèileachaidh" +Basic.Settings.Video.DisableAeroWindows="Cuir à comas Aero (Windows a-mhàin)" +Basic.Settings.Video.FPS="FPS" +Basic.Settings.Video.FPSCommon="Luachan FPS cumanta" +Basic.Settings.Video.FPSInteger="Luach FPS slàn" +Basic.Settings.Video.FPSFraction="Luachan FPS bloigheach" +Basic.Settings.Video.Numerator="Àireamhaiche" +Basic.Settings.Video.Denominator="Seòrsaiche" +Basic.Settings.Video.Renderer="Inneal-reandaraidh" +Basic.Settings.Video.InvalidResolution="Chan eil luach na dùmhlachd-breacaidh dligheach. Feumaidh e a bhith ’na [leud]x[àirde] (can 1920x1080)" +Basic.Settings.Video.CurrentlyActive="Tha às-chur video gnìomhach an-dràsta. cuir dheth gach às-chur airson roghainnean a’ video atharrachadh." +Basic.Settings.Video.DisableAero="Cuir à comas Aero" + +Basic.Settings.Video.DownscaleFilter.Bilinear="Dà-loidhneach (as luaithe ach sgleò air le sgèilachadh)" +Basic.Settings.Video.DownscaleFilter.Bicubic="Dà-chiùbach (sgèileachadh geuraichte, 16 sampallan)" +Basic.Settings.Video.DownscaleFilter.Lanczos="Lanczos (sgèileachadh geuraichte, 32 sampall)" + +Basic.Settings.Audio="Fuaim" +Basic.Settings.Audio.SampleRate="Reat shampallan" +Basic.Settings.Audio.Channels="Seanailean" +Basic.Settings.Audio.MeterDecayRate="Reat crìonaidh a’ mheidheadair-fhuaime" +Basic.Settings.Audio.MeterDecayRate.Fast="Luath" +Basic.Settings.Audio.MeterDecayRate.Medium="Meadhanach (PPM seòrsa I)" +Basic.Settings.Audio.MeterDecayRate.Slow="Slaodach (PPM seòrsa II)" +Basic.Settings.Audio.PeakMeterType="Seòrsa a’ mheidheadair-bharran" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Ball-sampaill de bharr" +Basic.Settings.Audio.PeakMeterType.TruePeak="Barr fìrinneach (cleachdadh nas àirde a’ CPU)" +Basic.Settings.Audio.MultiChannelWarning.Enabled="RABHADH: Tha fuaim cuairteachaidh an comas." +Basic.Settings.Audio.MultichannelWarning="Ma tha thu a’ dèanamh sruthadh, dearbh gun doir an t-seirbheis sruthaidh agad taic an dà chuid ri ion-chur is cluich fuaime cuairteachaidh. Mar eisimpleir, cuiridh Twitch, Facebook 360 Live, Mixer RTMP is Smashcast làn-taic ri fuaim cuairteachaidh. Ged a ghabhas Facebook Live is youTube Live ri sruthan fuaime cuairteachaidh, nì Facebook Live measgachadh sìos stereo dheth agus cha chluich YouTube live ach dà sheanail.\n\nTha criathragan fuaime OBS co-chòrdail ri fuaim cuairteachaidh ged nach doir sinn barantas gun obraich plugain VST." +Basic.Settings.Audio.MultichannelWarning.Title="A bheil thu airson fuaim cuairteachaidh a chur an comas?" +Basic.Settings.Audio.MultichannelWarning.Confirm="A bheil thu cinnteach gu bheil thu airson fuaim cuairteachaidh a chur an comas?" +Basic.Settings.Audio.DesktopDevice="Uidheam fuaime an deasg" +Basic.Settings.Audio.DesktopDevice2="Uidheam fuaime an deasg 2" +Basic.Settings.Audio.AuxDevice="Uidheam fuaime micreofoin/taice" +Basic.Settings.Audio.AuxDevice2="Uidheam fuaime micreofoin/taice 2" +Basic.Settings.Audio.AuxDevice3="Uidheam fuaime micreofoin/taice 3" +Basic.Settings.Audio.EnablePushToMute="Cuir an comas brùth-airson-mùchadh" +Basic.Settings.Audio.PushToMuteDelay="Dàil air brùth-airson-mùchadh" +Basic.Settings.Audio.EnablePushToTalk="Cuir an comas brùth-airson-bruidhinn" +Basic.Settings.Audio.PushToTalkDelay="Dàil air brùth-airson-bruidhinn" +Basic.Settings.Audio.UnknownAudioDevice="[Chan eil uidheam ceangailte no ri fhaighinn]" + +Basic.Settings.Advanced="Adhartach" +Basic.Settings.Advanced.General.ProcessPriority="Prìomhachas a’ phròiseis" +Basic.Settings.Advanced.General.ProcessPriority.High="Àrd" +Basic.Settings.Advanced.General.ProcessPriority.AboveNormal="Nas àirde na àbhaisteach" +Basic.Settings.Advanced.General.ProcessPriority.Normal="Àbhaisteach" +Basic.Settings.Advanced.General.ProcessPriority.BelowNormal="Nas ìsle na àbhaisteach" +Basic.Settings.Advanced.General.ProcessPriority.Idle="’Na tàmh" +Basic.Settings.Advanced.FormatWarning="Rabhadh: Chaidh fòrmatan datha seach NV12 a dhealbhachadh a chum clàraidh agus cha molamaid airson sruthadh iad. Cleachdaidh an sruthadh barrachd dhen CPU ri linn iompachadh air fòrmat nan dathan." +Basic.Settings.Advanced.Audio.BufferingTime="Ùine bufair na fuaime" +Basic.Settings.Advanced.Video.ColorFormat="Fòrmat nan dathan" +Basic.Settings.Advanced.Video.ColorSpace="Spàs dhathan YUV" +Basic.Settings.Advanced.Video.ColorRange="Rainse dhathan YUV" +Basic.Settings.Advanced.Video.ColorRange.Partial="Leth-phàirteach" +Basic.Settings.Advanced.Video.ColorRange.Full="Làn" +Basic.Settings.Advanced.Audio.MonitoringDevice="Uidheam sgrùdadh fuaime" +Basic.Settings.Advanced.Audio.MonitoringDevice.Default="Tùsail" +Basic.Settings.Advanced.Audio.DisableAudioDucking="Cuir à comas tumadh fuaime Windows" +Basic.Settings.Advanced.StreamDelay="Dàil an t-sruthaidh" +Basic.Settings.Advanced.StreamDelay.Duration="Faide (diog)" +Basic.Settings.Advanced.StreamDelay.Preserve="Glèidh puing a’ ghearraidh (meudaich an dàil) nuair a nithear ath-cheangal" +Basic.Settings.Advanced.StreamDelay.MemoryUsage="Tuairmse air cleachdadh na cuimhne: %1 MB" +Basic.Settings.Advanced.Network="Lìonra" +Basic.Settings.Advanced.Network.BindToIP="Nasg ri IP" +Basic.Settings.Advanced.Network.EnableNewSocketLoop="Cuir an comas an còd lìonraidh ùr" +Basic.Settings.Advanced.Network.EnableLowLatencyMode="Modh foillidheachd ìosail" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Cuir à comas na grad-iuchraichean nuair a bhios am fòcas air a’ phrìomh-uinneag" + +Basic.AdvAudio="Roghainnean adhartach na fuaime" +Basic.AdvAudio.Name="Ainm" +Basic.AdvAudio.Volume="Àirde (%)" +Basic.AdvAudio.Mono="Measgaich sìos gu mono" +Basic.AdvAudio.Panning="Panachadh" +Basic.AdvAudio.SyncOffset="Frìth-àireamh an t-sioncronachaidh (ms)" +Basic.AdvAudio.Monitoring="Sgrùdadh fuaime" +Basic.AdvAudio.Monitoring.None="Gun sgrùdadh" +Basic.AdvAudio.Monitoring.MonitorOnly="Sgrùdadh a-mhàin (mùch an t-às-chur)" +Basic.AdvAudio.Monitoring.Both="Sgrùdadh is às-chur" +Basic.AdvAudio.AudioTracks="Tracaichean" + +Basic.Settings.Hotkeys="Grad-iuchraichean" +Basic.Settings.Hotkeys.Pair="Nì na co-iuchraichean a tha ’gan co-roinneadh le “%1” toglachadh" + +Basic.Hotkeys.SelectScene="Gearr leum dhan t-sealladh" + +Basic.SystemTray.Show="Seall" +Basic.SystemTray.Hide="Falaich" + +Basic.SystemTray.Message.Reconnecting="Gun cheangal. ’Ga ath-cheangal…" + +Hotkeys.Insert="Cuir a-steach" +Hotkeys.Delete="Sguab às" +Hotkeys.Home="Home" +Hotkeys.End="End" +Hotkeys.PageUp="Page Up" +Hotkeys.PageDown="Page Down" +Hotkeys.NumLock="Num Lock" +Hotkeys.ScrollLock="Scroll Lock" +Hotkeys.CapsLock="Caps Lock" +Hotkeys.Backspace="Backspace" +Hotkeys.Tab="Tab" +Hotkeys.Print="Print" +Hotkeys.Pause="Pause" +Hotkeys.Left="Gu clì" +Hotkeys.Right="Gu deas" +Hotkeys.Up="Suas" +Hotkeys.Down="Sìos" +Hotkeys.Windows="Windows" +Hotkeys.Super="Super" +Hotkeys.Menu="Menu" +Hotkeys.Space="Space" +Hotkeys.NumpadNum="%1 air pada nan àireamh" +Hotkeys.NumpadMultiply="Iomadachadh air pada nan àireamh" +Hotkeys.NumpadDivide="Roinneadh air pada nan àireamh" +Hotkeys.NumpadAdd="Cur ris air pada nan àireamh" +Hotkeys.NumpadSubtract="Toir air falbh air pada nan àireamh" +Hotkeys.NumpadDecimal="Puing air pada nan àireamh" +Hotkeys.AppleKeypadNum="%1 (pada nan àireamh)" +Hotkeys.AppleKeypadMultiply="* (pada nan àireamh)" +Hotkeys.AppleKeypadDivide="/ (pada nan àireamh)" +Hotkeys.AppleKeypadAdd="+ (pada nan àireamh)" +Hotkeys.AppleKeypadSubtract="- (pada nan àireamh)" +Hotkeys.AppleKeypadDecimal=". (pada nan àireamh)" +Hotkeys.AppleKeypadEqual="= (pada nan àireamh)" +Hotkeys.MouseButton="%1 na luchaige" + +Mute="Mùch" +Unmute="Dì-mhùch" +Push-to-mute="Brùth-airson-mùchadh" +Push-to-talk="Brùth-airson-bruidhinn" + +SceneItemShow="Seall “%1”" +SceneItemHide="Falaich “%1”" + +OutputWarnings.NoTracksSelected="Feumaidh tu traca no dhà a thaghadh" +OutputWarnings.MultiTrackRecording="Rabhadh: Tha fòrmatan ann (can FLV) nach cuir taic ri iomadh traca sa chlàradh" +OutputWarnings.MP4Recording="Rabhadh: Cha ghabh clàraidhean a thèid a shàbhaladh gu MP4 aiseag mura gabh am faidhle a thoirt gu crìch (can ri linn tuisleachaidh, call cumhachd is msaa.). Nam bu toigh leat iomadh traca fuaime a chlàradh, mholamaid gun cleachd thu MKV agus gun iompaich thu an clàradh gu mp4 nuair a bhios e deiseil (Faidhle->Iompaich clàraidhean)" + +FinalScene.Title="Sguab às an sealladh" +FinalScene.Text="Feumaidh do shealladh a bhith ann." + + + + diff --git a/UI/data/locale/gl-ES.ini b/UI/data/locale/gl-ES.ini index 4411f70..c90dfc7 100644 --- a/UI/data/locale/gl-ES.ini +++ b/UI/data/locale/gl-ES.ini @@ -413,3 +413,6 @@ OutputWarnings.NoTracksSelected="Debes seleccionar, cando menos, unha pista" OutputWarnings.MultiTrackRecording="Aviso: certos formatos (caso de FLV) non admiten múltiples pistas para gravar" + + + diff --git a/UI/data/locale/he-IL.ini b/UI/data/locale/he-IL.ini index eec2909..0f8e6a0 100644 --- a/UI/data/locale/he-IL.ini +++ b/UI/data/locale/he-IL.ini @@ -508,10 +508,6 @@ Basic.Settings.General.SaveProjectors="שמור את המקרנים ביציאה Basic.Settings.General.SwitchOnDoubleClick="מעבר לסצנה על ידי הקלקה כפולה" Basic.Settings.General.StudioPortraitLayout="אפשר פריסה אנכית/דיוקן" Basic.Settings.General.MultiviewLayout="פריסת תצוגה מרובה" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="אופקי, עליון" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="אופקי, תחתון" -Basic.Settings.General.MultiviewLayout.Vertical.Left="אנכי, שמאל" -Basic.Settings.General.MultiviewLayout.Vertical.Right="אנכי, ימין" Basic.Settings.Stream="זרם נתונים" Basic.Settings.Stream.StreamType="סוג זרם נתונים" @@ -749,3 +745,6 @@ OutputWarnings.MP4Recording="אזהרה: הקלטות שנשמרו MP4 תהיה FinalScene.Title="מחק סצינה" FinalScene.Text="נדרשת סצנה אחת לפחות." + + + diff --git a/UI/data/locale/hi-IN.ini b/UI/data/locale/hi-IN.ini new file mode 100644 index 0000000..16c8600 --- /dev/null +++ b/UI/data/locale/hi-IN.ini @@ -0,0 +1,124 @@ + +Language="हिन्दी" +Region="इंडिया" + +OK="ठीक है" +Apply="लागू करें" +Cancel="रद्द करें" +Close="बंद करें" +Save="सहेजें" +Discard="छोड़ें" +Yes="हां" +No="नहीं" +Add="जोड़ें" +Remove="निकालें" +Rename="नाम बदलें" +Interact="बातचीत" +MoveUp="ऊपर ले जाएँ" +MoveDown="नीचे ले जाएँ" +Settings="सेटिंग्स" +Display="प्रदर्शन" +Name="नाम" +Exit="निकास" +Clear="साफ़ करें" +Revert="पहले जैसा करें" +Show="दिखाएँ" +Hide="छुपायें" +UnhideAll="कुछ ना छिपाएं" +New="नया" +Duplicate="दोहरा" +Left="बाएं" +Right="दाएँ" +Top="शीर्ष" +Bottom="नीचे" +Hours="घंटे" +Minutes="मिनट" +Seconds="सेकंड" +Copy="प्रतिलिपि" +Paste="चिपकाएँ" +Next="अगले" +Back="वापस" + + + +BandwidthTest.Region.Asia="एशिया" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/data/locale/hr-HR.ini b/UI/data/locale/hr-HR.ini index c7ca15a..744d7c6 100644 --- a/UI/data/locale/hr-HR.ini +++ b/UI/data/locale/hr-HR.ini @@ -604,3 +604,6 @@ OutputWarnings.NoTracksSelected="Morate odabrati makar jednu traku" OutputWarnings.MultiTrackRecording="Upozorenje: Određeni formati (kao što je FLV) ne podržavaju više traka po snimku" + + + diff --git a/UI/data/locale/hu-HU.ini b/UI/data/locale/hu-HU.ini index d8b91f0..6413a55 100644 --- a/UI/data/locale/hu-HU.ini +++ b/UI/data/locale/hu-HU.ini @@ -14,7 +14,7 @@ No="Nem" Add="Hozzáadás" Remove="Eltávolítás" Rename="Átnevezés" -Interact="Kölcsönhatás" +Interact="Ráhatás" Filters="Szűrők" Properties="Tulajdonságok" MoveUp="Mozgatás Fel" @@ -78,6 +78,8 @@ None="Nincs" StudioMode.Preview="Előnézet" StudioMode.Program="Program" ShowInMultiview="Mutatás Multiviewban" +VerticalLayout="Függőleges elrendezés" +Group="Csoport" AlreadyRunning.Title="Az OBS már fut" AlreadyRunning.Text="Az OBS már fut! Ha nem teljesen biztos benne mit tesz, akkor állítsa le az összes már futó OBS programot. Ha a programot úgy állította be, hogy rendszertálcára minimalizálódjon, akkor ellenőrizze, hogy ott megtalálható e." @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="Visszajátszás puffer leáll..." Basic.Main.StopStreaming="Stream leállítása" Basic.Main.StoppingStreaming="Stream leállítása..." Basic.Main.ForceStopStreaming="Stream leállítása (Késleltetés elvetése)" +Basic.Main.Group="Csoport %1" +Basic.Main.GroupItems="Kijelölt elemek csoportosítása" +Basic.Main.Ungroup="Csoport megszüntetése" Basic.MainMenu.File="&Fájl" Basic.MainMenu.File.Export="&Exportálás" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="&Eszközök" Basic.MainMenu.Help="&Segítség" Basic.MainMenu.Help.HelpPortal="Segítség &Portál" Basic.MainMenu.Help.Website="Weboldal megtekintése" +Basic.MainMenu.Help.Discord="Csatlakozás &Discord szerverre" Basic.MainMenu.Help.Logs="&Naplófájlok" Basic.MainMenu.Help.Logs.ShowLogs="&Naplófájlok megjelenítése" Basic.MainMenu.Help.Logs.UploadCurrentLog="&Aktuális Naplófájl feltöltése" Basic.MainMenu.Help.Logs.UploadLastLog="&Utolsó Naplófájl feltöltése" Basic.MainMenu.Help.Logs.ViewCurrentLog="&Jelenlegi Naplófájl megtekintése" Basic.MainMenu.Help.CheckForUpdates="Frissítések ellenőrzése" +Basic.MainMenu.Help.CrashLogs="Hibajelentések (&R)" +Basic.MainMenu.Help.CrashLogs.ShowLogs="Hibajelentések megjelenítése (&S)" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="Utolsó Hibajelentés feltöltése (&L)" Basic.Settings.ProgramRestart="A beállítások érvénybe lépéséhez a program újraindítása szükséges." Basic.Settings.ConfirmTitle="Változtatások megerősítése" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Mindig a rendszertálcára minima Basic.Settings.General.SaveProjectors="Projektorok mentése kilépéskor" Basic.Settings.General.SwitchOnDoubleClick="Átmenet a jelenetre dupla kattintás esetén" Basic.Settings.General.StudioPortraitLayout="Portré/függőleges elrendezés engedélyezése" +Basic.Settings.General.Multiview="MultiView" +Basic.Settings.General.Multiview.MouseSwitch="Kattintás a jelenetek közötti váltáshoz" +Basic.Settings.General.Multiview.DrawSourceNames="Jelenetek neveinek megjelenítése" +Basic.Settings.General.Multiview.DrawSafeAreas="Biztonságos területek kirajzolása (EBU R 95)" Basic.Settings.General.MultiviewLayout="MultiView elrendezés" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Vízszintes, Felső" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Vízszintes, Alsó" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Függőleges, Bal" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Függőleges, Jobb" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Vízszintes, felső (8 jelenet)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Vízszintes, alsó (8 jelenet)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Függőleges, bal (8 jelenet)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Függőleges, jobb (8 jelenet)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Vízszintes, felső (24 jelenet)" Basic.Settings.Stream="Stream" Basic.Settings.Stream.StreamType="Stream típusa" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="Hangmérő halkulási aránya" Basic.Settings.Audio.MeterDecayRate.Fast="Gyors" Basic.Settings.Audio.MeterDecayRate.Medium="Medium (Típus | PPM)" Basic.Settings.Audio.MeterDecayRate.Slow="Lassú (Típus II PPM)" +Basic.Settings.Audio.PeakMeterType="Csúcsmérték Típus" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Csúcsminta" +Basic.Settings.Audio.PeakMeterType.TruePeak="Valós Csúcs (Magasabb CPU használat)" Basic.Settings.Audio.MultiChannelWarning.Enabled="Figyelmeztetés: Surround sound hang engedélyezve van." Basic.Settings.Audio.MultichannelWarning="Ha közvetít, ellenőrizze, hogy a stream szolgáltatója támogatja mind a surround sound kezelést, mind pedig a surround sound lejátszást. Twitch, Facebook 360 Live, Mixer RTMP, Smashcast a legjobb példa, hogy mely platformokon van teljes támogatás. Ellenben a Facebook Live és a YouTube Live minden elfogadja a térhangzású hangot, a Facebook Live lekeveri stereora és a YouTube Live csak két csatornát játszik le.\n\nOBS audio szűrők kompatibilisek a térhangzású hanggal, viszont a VST bővítmények támogatása nem garantált." Basic.Settings.Audio.MultichannelWarning.Title="Engedélyezi a surround hangzást?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="Hálózat" Basic.Settings.Advanced.Network.BindToIP="IP-hez rendelés" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Új hálózatkezelő kód engedélyezése" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Alacsony késleltetésű mód" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Gyorsbillentyűk letiltása, ha a fő ablak fókuszban van" Basic.AdvAudio="Speciális hangtulajdonságok" Basic.AdvAudio.Name="Név" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="Figyelem: Az MP4-be mentett állományok javíthata FinalScene.Title="Jelenet törlése" FinalScene.Text="Legalább egy jelenetnek lennie kell." +NoSources.Title="Nincsenek Források" +NoSources.Text="Úgy fest, hogy nem adott hozzá valamilyen videoforrást, úgyhogy fekete képet fog adni. Biztos benne, hogy ezt kívánja tenni?" +NoSources.Text.AddSource="Hozzáadhat forrásokat bármikor a + ikonra kattintva a Források doboz alatt a fő ablakban." + +ChangeBG="Szín megadása" +CustomColor="Egyedi szín" + +BrowserSource.EnableHardwareAcceleration="Böngészőforrás hardveres támogatásának engedélyezése" + diff --git a/UI/data/locale/it-IT.ini b/UI/data/locale/it-IT.ini index 39d72e9..2a121b4 100644 --- a/UI/data/locale/it-IT.ini +++ b/UI/data/locale/it-IT.ini @@ -78,6 +78,8 @@ None="Nessuno" StudioMode.Preview="Anteprima" StudioMode.Program="Programma" ShowInMultiview="Mostra in Vista-Multipla" +VerticalLayout="Layout verticale" +Group="Gruppo" AlreadyRunning.Title="OBS è già in esecuzione" AlreadyRunning.Text="OBS è già in esecuzione! A meno che non si intendeva effettuare questa operazione, chiudere tutte le istanze esistenti di OBS prima di provare a eseguirne una nuova. Se avete OBS impostato per minimizzarsi nell'area di notifica, si prega di controllare per vedere se è ancora in esecuzione." @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="Arresto del buffer di riproduzione in corso..." Basic.Main.StopStreaming="Ferma trasmissione" Basic.Main.StoppingStreaming="Arresto trasmissione..." Basic.Main.ForceStopStreaming="Ferma trasmissione (annulla ritardo)" +Basic.Main.Group="Gruppo %1" +Basic.Main.GroupItems="Elementi selezionati" +Basic.Main.Ungroup="Separa" Basic.MainMenu.File="&File" Basic.MainMenu.File.Export="&Esporta" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="&Strumenti" Basic.MainMenu.Help="&Aiuto" Basic.MainMenu.Help.HelpPortal="Portale Aiuto" Basic.MainMenu.Help.Website="Visita il sito" +Basic.MainMenu.Help.Discord="Join & Discord Server" Basic.MainMenu.Help.Logs="File di &log" Basic.MainMenu.Help.Logs.ShowLogs="&Visualizza i file di Log" Basic.MainMenu.Help.Logs.UploadCurrentLog="Carica file di log &corrente" Basic.MainMenu.Help.Logs.UploadLastLog="Carica u<imo file di log" Basic.MainMenu.Help.Logs.ViewCurrentLog="&Vedi attuale file di log" Basic.MainMenu.Help.CheckForUpdates="Controlla aggiornamenti" +Basic.MainMenu.Help.CrashLogs="Segnalazione c&rash " +Basic.MainMenu.Help.CrashLogs.ShowLogs="Vi&sualizza Segnalazione crash" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="Carica l'u<imo Crash Report" Basic.Settings.ProgramRestart="Il programma deve essere riavviato perché questi cambiamenti abbiano effetto." Basic.Settings.ConfirmTitle="Conferma cambiamenti" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Minimizza sempre nel vassoio di s Basic.Settings.General.SaveProjectors="Salva i proiettori all'uscita" Basic.Settings.General.SwitchOnDoubleClick="Transizione alla scena al doppio-click" Basic.Settings.General.StudioPortraitLayout="Attiva il layout Orizzontale/Verticale" +Basic.Settings.General.Multiview="Visualizzazione Multipla" +Basic.Settings.General.Multiview.MouseSwitch="Clicca per passare da una scena all'altra" +Basic.Settings.General.Multiview.DrawSourceNames="Visualizza il nome della scena" +Basic.Settings.General.Multiview.DrawSafeAreas="Evidenziare aree sicure (EBU R 95)" Basic.Settings.General.MultiviewLayout="Layout a viste multiple" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Orizzontale, Alto" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Orizzontale, Basso" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Orizzontale, Sinistra" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Orizzontale, Destra" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Orizzontale, In alto (8 scene)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Orizzontale, In basso (8 scene)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Verticale, A sinistra (8 scene)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Verticale, A destra (8 scene)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Orizzontale, In alto (24 scene)" Basic.Settings.Stream="Trasmissione" Basic.Settings.Stream.StreamType="Tipo di stream" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="Tasso di decadimento audio" Basic.Settings.Audio.MeterDecayRate.Fast="Veloce" Basic.Settings.Audio.MeterDecayRate.Medium="Medio (Tipo 1 PPM)" Basic.Settings.Audio.MeterDecayRate.Slow="Lento (Tipo 2 PPM)" +Basic.Settings.Audio.PeakMeterType="Modalità Peak Meter" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Sample Peak" +Basic.Settings.Audio.PeakMeterType.TruePeak="True Peak (alto utilizzo della CPU)" Basic.Settings.Audio.MultiChannelWarning.Enabled="ATTENZIONE: L'audio Surround è attivo." Basic.Settings.Audio.MultichannelWarning="Per lo streaming, accertati che il servizio di streaming supporti sia integrazione che riproduzione di suono surround. Twitch, Facebook 360 Live, Mixer RTMP e Smashcast sono esempi di servizi in cui il suono surround è completamente supportato. Anche se sia Facebook Live che YouTube Live implementano il suono surround, Facebook Live lo converte in stereo, mentre YouTube Live ne riproduce solo due canali.\n\nI filtri di OBS Studio sono compatibili con il suono surround, anche se il supporto per il plugin VST non è garantito." Basic.Settings.Audio.MultichannelWarning.Title="Abilitare l'audio surround?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="Rete" Basic.Settings.Advanced.Network.BindToIP="Associa a IP" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Attiva il nuovo codice di rete" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Modalità a bassa latenza" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Disabilitare i tasti di scelta rapida quando la finestra principale è a fuoco" Basic.AdvAudio="Proprietà audio avanzate" Basic.AdvAudio.Name="Nome" @@ -749,3 +767,7 @@ OutputWarnings.MP4Recording="Avviso: le registrazioni salvate in MP4 non saranno FinalScene.Title="Elimina scena" FinalScene.Text="Deve esserci almeno una scena." +NoSources.Title="Nessuna sorgente" + + + diff --git a/UI/data/locale/ja-JP.ini b/UI/data/locale/ja-JP.ini index ff668b1..6dc4ddc 100644 --- a/UI/data/locale/ja-JP.ini +++ b/UI/data/locale/ja-JP.ini @@ -78,6 +78,8 @@ None="未設定" StudioMode.Preview="プレビュー" StudioMode.Program="番組" ShowInMultiview="マルチビューで表示" +VerticalLayout="垂直レイアウト" +Group="グループ化" AlreadyRunning.Title="OBSは既に実行中です" AlreadyRunning.Text="OBSは既に実行されています! この操作を行うつもりがない限り、新しいインスタンスを実行する前に既存のOBSインスタンスを終了してください。OBSがシステムトレイに最小化されるように設定されている場合は、まだ実行中であるかどうかを確認してください。" @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="リプレイバッファー停止処理中..." Basic.Main.StopStreaming="配信終了" Basic.Main.StoppingStreaming="配信停止処理中..." Basic.Main.ForceStopStreaming="配信停止 (遅延破棄)" +Basic.Main.Group="グループ化 %1" +Basic.Main.GroupItems="選択したアイテムのグループ化" +Basic.Main.Ungroup="グループ化の解除" Basic.MainMenu.File="ファイル(&F)" Basic.MainMenu.File.Export="エクスポート(&E)" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="ツール(&T)" Basic.MainMenu.Help="ヘルプ(&H)" Basic.MainMenu.Help.HelpPortal="ヘルプポータル(&P)" Basic.MainMenu.Help.Website="ウェブサイト(&W)" +Basic.MainMenu.Help.Discord="Discordサーバーに参加(&D)" Basic.MainMenu.Help.Logs="ログファイル(&L)" Basic.MainMenu.Help.Logs.ShowLogs="ログファイルを表示(&S)" Basic.MainMenu.Help.Logs.UploadCurrentLog="現在のログファイルをアップロード(&C)" Basic.MainMenu.Help.Logs.UploadLastLog="最新のログファイルをアップロード(&L)" Basic.MainMenu.Help.Logs.ViewCurrentLog="現在のログを表示(&V)" Basic.MainMenu.Help.CheckForUpdates="更新を確認" +Basic.MainMenu.Help.CrashLogs="クラッシュレポート(&R)" +Basic.MainMenu.Help.CrashLogs.ShowLogs="クラッシュレポートを表示(&S)" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="最新のクラッシュレポートをアップロード(&L)" Basic.Settings.ProgramRestart="これらの設定を有効にするためにはプログラムの再起動が必要です。" Basic.Settings.ConfirmTitle="変更確認" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="タスクバーの代わりにシ Basic.Settings.General.SaveProjectors="終了時にプロジェクターを保存する" Basic.Settings.General.SwitchOnDoubleClick="ダブルクリックしたときにシーンに遷移" Basic.Settings.General.StudioPortraitLayout="縦長/垂直レイアウトを有効にする" +Basic.Settings.General.Multiview="マルチビュー" +Basic.Settings.General.Multiview.MouseSwitch="クリックするとシーンを切り替える" +Basic.Settings.General.Multiview.DrawSourceNames="シーン名を表示" +Basic.Settings.General.Multiview.DrawSafeAreas="安全領域を描画 (EBU R95)" Basic.Settings.General.MultiviewLayout="マルチビューレイアウト" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="水平, 上" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="水平, 下" -Basic.Settings.General.MultiviewLayout.Vertical.Left="垂直, 左" -Basic.Settings.General.MultiviewLayout.Vertical.Right="垂直, 右" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="水平上側 (8 シーン)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="水平下側 (8 シーン)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="垂直左側 (8 シーン)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="垂直右側 (8 シーン)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="水平上側 (24 シーン)" Basic.Settings.Stream="配信" Basic.Settings.Stream.StreamType="配信種別" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="音声メーターの減衰率" Basic.Settings.Audio.MeterDecayRate.Fast="速い" Basic.Settings.Audio.MeterDecayRate.Medium="中 (タイプ I PPM)" Basic.Settings.Audio.MeterDecayRate.Slow="遅い (タイプ II PPM)" +Basic.Settings.Audio.PeakMeterType="ピークメーターの種類" +Basic.Settings.Audio.PeakMeterType.SamplePeak="サンプル ピーク" +Basic.Settings.Audio.PeakMeterType.TruePeak="真のピーク (CPU使用率高い)" Basic.Settings.Audio.MultiChannelWarning.Enabled="警告: サラウンド音声が有効です。" Basic.Settings.Audio.MultichannelWarning="配信する場合、配信サービスがサラウンド音声の取り込みと再生の両方をサポートしているかどうかを確認してください。Twitch、Facebook 360 Live、Mixer RTMP、Smashcastは、サラウンド音声が完全にサポートされている例です。しかしFacebook LiveとYouTube Liveはどちらもサラウンド取り込みを受信しますが、Facebook Liveはステレオにダウンミックスし、YouTube Liveは2チャンネルのみしか再生できません。\n\nVSTプラグインのサポートは保証されていませんが、OBS音声フィルタはサラウンド音声と互換性があります。" Basic.Settings.Audio.MultichannelWarning.Title="サラウンド音声を有効にしますか?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="ネットワーク" Basic.Settings.Advanced.Network.BindToIP="IP選択" Basic.Settings.Advanced.Network.EnableNewSocketLoop="新しいネットワークコードを有効にする" Basic.Settings.Advanced.Network.EnableLowLatencyMode="低遅延モード" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="メインウィンドウにフォーカスがあるときはホットキーを無効にする" Basic.AdvAudio="オーディオの詳細プロパティ" Basic.AdvAudio.Name="名称" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="警告: ファイルをファイナライズ出来 FinalScene.Title="シーンを削除する" FinalScene.Text="1つ以上のシーンが必要です。" +NoSources.Title="ソース無し" +NoSources.Text="映像ソースをまだ追加していないようなので、空白の画面だけが出力されます。よろしいですか?" +NoSources.Text.AddSource="メインウィンドウのソースボックスの下にある + アイコンをクリックすると、いつでもソースを追加することができます。" + +ChangeBG="色の設定" +CustomColor="カスタム色" + +BrowserSource.EnableHardwareAcceleration="ブラウザソースのハードウェアアクセラレーションを有効にする" + diff --git a/UI/data/locale/ka-GE.ini b/UI/data/locale/ka-GE.ini index d446611..eb59e36 100644 --- a/UI/data/locale/ka-GE.ini +++ b/UI/data/locale/ka-GE.ini @@ -2,7 +2,7 @@ Language="ქართული" Region="საქართველო" -OK="კაი" +OK="კარგი" Apply="მიღება" Cancel="გაუქმება" Close="დახურვა" @@ -28,6 +28,16 @@ Browse="მოძიება" Mono="მონო" Stereo="სტერეო" DroppedFrames="კადრების ვარდნა %1 (%2%)" +StudioProgramProjector="სრულეკრანიანი ჩვენება (შედეგი)" +PreviewProjector="სრულეკრანიანი ჩვენება (შეთვალიერება)" +SceneProjector="სრულეკრანიანი ჩვენება (სცენა)" +SourceProjector="სრულეკრანიანი ჩვენება (წყარო)" +StudioProgramWindow="ფანჯარაში ჩვენება (შედეგი)" +PreviewWindow="ფანჯარაში ჩვენება (შეთვალიერება)" +SceneWindow="ფანჯარაში ჩვენება (სცენა)" +SourceWindow="ფანჯარაში ჩვენება (წყარო)" +MultiviewProjector="მრავალხედიანი (სრულ ეკრანზე)" +MultiviewWindowed="მრავალხედიანი (ფანჯარაში)" Clear="გასუფთავება" Revert="დაბრუნება" Show="ჩვენება" @@ -56,123 +66,713 @@ Import="შემოტანა" Export="გატანა" Copy="დაკოპირება" Paste="ჩასმა" +PasteReference="ჩასმა (ბმული)" +PasteDuplicate="ჩასმა (ასლი)" +RemuxRecordings="ჩანაწერების გარდაქმნა" Next="შემდეგ" Back="უკან" Defaults="ნაგულისხმევი" +HideMixer="ხმის მიქშერში დამალვა" +TransitionOverride="გადასვლა გადაფარვით" +None="არცერთი" +StudioMode.Preview="შეთვალიერება" +StudioMode.Program="შედეგი" +ShowInMultiview="მრავალხედიანი ჩვენება" +VerticalLayout="შვეული განლაგება" +Group="დაჯგუფება" AlreadyRunning.Title="OBS უკვე გაშვებულია" AlreadyRunning.Text="OBS უკვე გაშვებულია! გთხოვთ, ჯერ დახუროთ OBS-ის ყველა გაშვებული პროცესი, სანამ ახლის გაშვებას შეეცდებით. თუ მითითებული გაქვთ, რომ დახურვის ნაცვლად, OBS სისტემურ არეში უნდა ჩაიკეცოს, გთხოვთ მანდაც გადაამოწმოთ, დარჩენილი ხომ არაა." AlreadyRunning.LaunchAnyway="მაინც გაშვება" Copy.Filters="ფილტრების დაკოპირება" +Paste.Filters="ფილტრების ჩასმა" +BandwidthTest.Region="რეგიონი" +BandwidthTest.Region.US="შეერთებული შტატები" +BandwidthTest.Region.EU="ევროპა" +BandwidthTest.Region.Asia="აზია" +BandwidthTest.Region.Other="სხვა" +Basic.FirstStartup.RunWizard="გსურთ, გაეშვას თვითგამართვის მეგზური? ამასთან, შეგიძლიათ პარამეტრების ხელით გამართვა მთავარ ფანჯარაში, პარამეტრების ღილაკზე დაწკაპებით." +Basic.FirstStartup.RunWizard.BetaWarning="(შენიშვნა: თვითგამართვის მეგზური ჯერჯერობით საცდელია)" +Basic.FirstStartup.RunWizard.NoClicked="თუ გადაიფიქრებთ, თვითგამართვის გაშვება შეგეძლებათ ნებისმიერ დროს, ხელსაწყოების მენიუდან." +Basic.AutoConfig="თვითგამართვის მეგზური" +Basic.AutoConfig.Beta="თვითგამართვის მეგზური (Beta)" +Basic.AutoConfig.ApplySettings="პარამეტრების მიღება" +Basic.AutoConfig.StartPage="გამოყენების შესახებ" +Basic.AutoConfig.StartPage.SubTitle="მიუთითეთ, თუ რა მიზნით გსურთ პროგრამის გამოყენება" +Basic.AutoConfig.StartPage.PrioritizeStreaming="ნაკადების გაშვებისთვის მორგება, ვიდეოს ჩაწერა მეორეხარისხოვანია" +Basic.AutoConfig.StartPage.PrioritizeRecording="ვიდეოების ჩაწერისთვის მორგება, ნაკადების გაშვებას არ ვაპირებ" +Basic.AutoConfig.VideoPage="ვიდეოს პარამეტრები" +Basic.AutoConfig.VideoPage.SubTitle="მიუთითეთ სასურველი ვიდეო-პარამეტრები" +Basic.AutoConfig.VideoPage.BaseResolution.UseCurrent="მიმდინარეს გამოყენება (%1x%2)" +Basic.AutoConfig.VideoPage.BaseResolution.Display="ეკრანი %1 (%2x%3)" +Basic.AutoConfig.VideoPage.FPS.UseCurrent="მიმდინარეს გამოყენება (%1)" +Basic.AutoConfig.VideoPage.FPS.PreferHighFPS="60 ან 30, თუმცა უმჯობესია 60, როცა შესაძლებელია" +Basic.AutoConfig.VideoPage.FPS.PreferHighRes="60 ან 30, თუმცა უმჯობესია მაღალი გარჩევადობით" +Basic.AutoConfig.VideoPage.CanvasExplanation="შენიშვნა: ეკრანის ფონის (ძირითადი) გაფართოება არაა აუცილებელი გაშვებული ნაკადის ან გადაღებული ვიდეოს გაფართოებას ემთხვეოდეს. ცალკეული ნაკადის/ვიდეოს ზომები შეიძლება შემცირდეს, რესურსების მოხმარების ან ბიტური სიხშირის შესამცირებლად." +Basic.AutoConfig.StreamPage="ნაკადის მონაცემები" +Basic.AutoConfig.StreamPage.SubTitle="გთხოვთ მიუთითოთ ნაკადის მონაცემები" +Basic.AutoConfig.StreamPage.Service="მომსახურება" +Basic.AutoConfig.StreamPage.Service.ShowAll="ყველას ჩვენება..." +Basic.AutoConfig.StreamPage.Server="სერვერი" +Basic.AutoConfig.StreamPage.StreamKey="ნაკადის გასაღები" +Basic.AutoConfig.StreamPage.StreamKey.LinkToSite="(ბმული)" +Basic.AutoConfig.StreamPage.PerformBandwidthTest="მიახლოებითი ბიტური სიხშირე, გამოთვლილი ქსელის გამტარუნარიანობის შემოწმებით (რამდენიმე წუთს შესაძლოა გასტანოს)" +Basic.AutoConfig.StreamPage.PreferHardwareEncoding="აპარატურული დაშიფვრის გამოყენება" +Basic.AutoConfig.StreamPage.PreferHardwareEncoding.ToolTip="აპარატურული დაშიფვრა შეამცირებს პროცესორზე დატვირთვას, თუმცა შესაძლოა მეტი ბიტური სიხშირე დასჭირდეს, იმავე ხარისხის მისაღწევად." +Basic.AutoConfig.StreamPage.StreamWarning.Title="გაფრთხილება ნაკადის გაშვებისას" +Basic.AutoConfig.StreamPage.StreamWarning.Text="ქსელის გამტარუნარიანობის შემოწმება გულისხმობს, თქვენს არხზე შემთხვევითი ვიდეოფაილების ხმის გაშერე ნაკადად გაშვებას. სასურველია, თუ დროებით გათიშავთ ნაკადის შენახვის შესაძლებლობას და შემოწმების დასრულებამდე ამ ნაკადს გადაიყვანთ პირად რეჟიმში. გსურთ, განაგრძოთ?" +Basic.AutoConfig.TestPage="საბოლოო შედეგები" +Basic.AutoConfig.TestPage.SubTitle.Testing="პროგრამა ახლა უშვებს სხვადასხვა სახის შემოწმებებს, სასურველი პარამეტრების დადგენის მიზნით" +Basic.AutoConfig.TestPage.SubTitle.Complete="შემოწმება დასრულებულია" +Basic.AutoConfig.TestPage.TestingBandwidth="ქსელის გამტარუნარიანობის შემოწმება, შესაძლოა რამდენიმე წუთს გასტანოს..." +Basic.AutoConfig.TestPage.TestingBandwidth.Connecting="უკავშირდება სერვერს: %1..." +Basic.AutoConfig.TestPage.TestingBandwidth.ConnectFailed="ვერცერთ სერვერთან დაკავშირება ვერ მოხერხდა. შეამოწმეთ თქვენი ინტერნეტთან კავშირი და სცადეთ ხელახლა." +Basic.AutoConfig.TestPage.TestingBandwidth.Server="გამტარუნარიანობის შემოწმება სერვერისთვის: %1" +Basic.AutoConfig.TestPage.TestingStreamEncoder="ნაკადის დამშიფრავის შემოწმება, შესაძლოა ერთ წუთამდე გასტანოს..." +Basic.AutoConfig.TestPage.TestingRecordingEncoder="ჩანაწერის დამშიფრავის შემოწმება, შესაძლოა ერთ წუთამდე გასტანოს..." +Basic.AutoConfig.TestPage.TestingRes="გაფართოებების შემოწმება, შესაძლოა რამდენიმე წუთს გასტანოს..." +Basic.AutoConfig.TestPage.TestingRes.Fail="დამშიფრავის ჩართვა ვერ მოხერხდა" +Basic.AutoConfig.TestPage.TestingRes.Resolution="მოწმდება %1x%2 %3 FPS (კადრ/წმ)..." +Basic.AutoConfig.TestPage.Result.StreamingEncoder="ნაკადის დამშიფრავი" +Basic.AutoConfig.TestPage.Result.RecordingEncoder="ჩანაწერის დამშიფრავი" +Basic.AutoConfig.TestPage.Result.Header="პროგრამის მიერ დადგენილი მიახლოებითი პარამეტრები, რომელიც მეტად გამოსადეგია თქვენთვის:" +Basic.AutoConfig.TestPage.Result.Footer="თუ გსურთ ამ პარამეტრების გამოყენება, დააწკაპეთ „პარამეტრების მიღებას“. თუ გსურთ პარამეტრების ხელახლა დადგენა, დააწკაპეთ ღილაკს „უკან“. ხოლო, თუ პარამეტრების ხელით გამართვა გსურთ, დააწკაპეთ „გაუქმებას“ და გადადით პარამეტრებზე." +Basic.Stats="სტატისტიკა" +Basic.Stats.CPUUsage="CPU დატვირთვა" +Basic.Stats.HDDSpaceAvailable="ხელმისაწვდომი ადგილი დისკზე" +Basic.Stats.MemoryUsage="მეხსიერების დატვირთვა" +Basic.Stats.AverageTimeToRender="კადრის დამუშავების საშუალო დრო" +Basic.Stats.SkippedFrames="დაშიფვრის დაყოვნების გამო გამოტოვებული კადრები" +Basic.Stats.MissedFrames="დამუშავების დაყოვნების გამო გამოტოვებული კადრები" +Basic.Stats.Output.Stream="ნაკადი" +Basic.Stats.Output.Recording="ჩაწერა" +Basic.Stats.Status="მდგომარეობა" +Basic.Stats.Status.Recording="მიმდინარეობს" +Basic.Stats.Status.Live="ეთერშია" +Basic.Stats.Status.Reconnecting="ხელახლა დაკავშირება" Basic.Stats.Status.Inactive="არააქტიური" +Basic.Stats.DroppedFrames="გამოტოვებული კადრები (ქსელი)" +Basic.Stats.MegabytesSent="საერთო მოცულობა" +Basic.Stats.Bitrate="ბიტური სიხშირე" +Updater.Title="ხელმისაწვდომია განახლება" +Updater.Text="ხელმისაწვდომია განახლება:" Updater.UpdateNow="განაახლე ახლა" +Updater.RemindMeLater="მოგვიანებით შეხსენება" +Updater.Skip="ვერსიის გამოტოვება" +Updater.Running.Title="პროგრამა ამჟამად გაშვებულია" +Updater.Running.Text="გაშვებულია გამომავალი სიგნალები, გთხოვთ, ჯერ გამორთოთ ყველა მოქმედი გამომავალი სიგნალი, სანამ განახლებას შეეცდებით" +Updater.NoUpdatesAvailable.Title="განახლებები არაა ხელმისაწვდომი" +Updater.NoUpdatesAvailable.Text="ამჟამად, განახლებები არაა ხელმისაწვდომი" +Updater.FailedToLaunch="განახლების გაშვება ვერ მოხერხდა" +Updater.GameCaptureActive.Title="მიმდინარეობს თამაშის ჩაწერა" +Updater.GameCaptureActive.Text="ამჟამად გაშვებულია თამაშის ჩამწერი ბიბლიოთეკა. გთხოვთ, შეწყვიტოთ ყველა თამაშის/პროგრამის ჩაწერა (ან თავიდან ჩართოთ windows) და სცადოთ ხელახლა." +QuickTransitions.SwapScenes="გადასვლის შემდეგ, შესათვალიერებელი და გამომავალი სცენებისთვის ადგილის გაცვლა" +QuickTransitions.SwapScenesTT="გადასვლის შემდეგ, შესათვალიერებელი და გამომავალი სცენებისთვის ადგილის გაცვლა (თუ ჯერ კიდევ არსებობს გამომავალი სცენის თავდაპირველი ასლი).\nამის შედეგად, გამომავალი სცენის თავდაპირველ ვარიანტზე გაკეთებული ცვლილებები, არ გაუქმდება." +QuickTransitions.DuplicateScene="სცენის გაორკეცება" +QuickTransitions.DuplicateSceneTT="ერთი და იმავე სცენის შესწორებისას, საშუალებას იძლევა წყარო გარდაიქმნას, გამომავალი შედეგის შეცვლის გარეშე.\nწყაროს პარამეტრების ჩასასწორებლად, გამომავალი შედეგის შეცვლის გარეშე, ჩართეთ 'წყაროს გაორკეცება'\nმოცემული მნიშვნელობის ცვლილება გამოიწვევს მიმდინარე გამომავალი სცენის საწყისზე დაბრუნებას (თუ ჯერ კიდევ არსებობს)." +QuickTransitions.EditProperties="წყაროს გაორკეცება" +QuickTransitions.EditPropertiesTT="ერთი და იმავე სცენის შესწორებისას, საშუალებას იძლევა წყაროს პარამეტრები ჩასწორდეს, გამომავალი შედეგის შეცვლის გარეშე.\nმისი გამოყენება შესაძლებელია, მხოლოდ 'სცენის გაორკეცების' შემთხვევაში\nცალკეული წყაროები (მათ შორის გადაღებული ან არსებული მედიაფაილები) არ იძლევა ამის საშუალებას და მათი განცალკევებით ჩასწორება შეუძლებელია.\nმოცემული მნიშვნელობის ცვლილება გამოიწვევს მიმდინარე გამომავალი სცენის საწყისზე დაბრუნებას (თუ ჯერ კიდევ არსებობს).\n\nგაფრთხილება: წყაროს გაორკეცების შედეგად, შესაძლოა სისტემის და ვიდეოდაფის დატვირთვა გაიზარდოს." +QuickTransitions.HotkeyName="სწრაფი გადასვლა: %1" +Basic.AddTransition="გასამართი გადასვლის დამატება" +Basic.RemoveTransition="გასამართი გადასვლის ამოშლა" +Basic.TransitionProperties="გადასვლის პარამეტრები" +Basic.SceneTransitions="სცენებს შორის გადასვლები" Basic.TransitionDuration="ხანგრძლივობა" +Basic.TogglePreviewProgramMode="სტუდიური რეჟიმი" +TransitionNameDlg.Text="გთხოვთ, მიუთითოთ გადასვლის სახელი" +TransitionNameDlg.Title="გადასვლის სახელი" +TitleBar.Profile="პროფილი" +TitleBar.Scenes="სცენები" +NameExists.Title="სახელი უკვე არსებობს" +NameExists.Text="სახელი უკვე გამოყენებულია." +NoNameEntered.Title="გთხოვთ, შეიყვანოთ მართებული სახელი" +NoNameEntered.Text="ცარიელი სახელი დაუშვებელია." +ConfirmStart.Title="გაეშვას ნაკადი?" +ConfirmStart.Text="ნამდვილად გსურთ პირდაპირი ეთერის გაშვება?" +ConfirmStop.Title="შეწყდეს ნაკადი?" +ConfirmStop.Text="ნამდვილად გსურთ პირდაპირი ეთერის შეწყვეტა?" ConfirmExit.Title="OBS-დან გასვლა?" +ConfirmExit.Text="OBS ამჟამად მოქმედია. ყველა გაშვებული ნაკადი/ჩაწერა შეწყდება. ნამდვილად გსურთ გამოსვლა?" +ConfirmRemove.Title="წაშლის დადასტურება" +ConfirmRemove.Text="ნამდვილად გსურთ, წაიშალოს '$1'?" +ConfirmRemove.TextMultiple="ნამდვილად გსურთ, წაიშალოს %1 მათგანი?" +Output.StartStreamFailed="ნაკადის გაშვება ვერ მოხერხდა" +Output.StartRecordingFailed="ჩაწერის დაწყება ვერ მოხერხდა" +Output.StartReplayFailed="გადახვევის ჩართვა ვერ მოხერხდა" +Output.StartFailedGeneric="სიგნალის გაშვება ვერ მოხერხდა. გთხოვთ, შეამოწმეთ აღრიცხვის ფაილი, დამატებითი ინფორმაციისთვის.\n\nშენიშვნა: თუ იყენებთ NVENC ან AMD დამშიფრავებს, დარწმუნდით რომ თქვენი ვიდეოდაფის პროგრამა განახლებულია." +Output.ConnectFail.Title="დაკავშირება ვერ მოხერხდა" +Output.ConnectFail.BadPath="არამართებული მისამართი ან დასაკავშირებელი URL ბმული. გთხოვთ, გადაამოწმოთ თქვენი პარამეტრების სისწორე." +Output.ConnectFail.ConnectFailed="სერვერთან დაკავშირება ვერ მოხერხდა" +Output.ConnectFail.InvalidStream="ვერ ხერხდება მითითებულ არხთან ან ნაკადის გასაღებთან დაკავშირება, გთხოვთ, გადაამოწმოთ თქვენი ნაკადის გასაღები. თუ სწორია, მაშინ შესაძლოა ხარვეზი იყოს სერვერთან დაკავშირებისას." +Output.ConnectFail.Error="მოულოდნელი შეცდომა წარმოიქმნა სერვერთან დაკავშირებისას. დამატებითი ინფორმაციისთვის იხილეთ აღრიცხვის ფაილი." +Output.ConnectFail.Disconnected="სერვერთან კავშირი გაწყვეტილია." +Output.RecordFail.Title="ჩაწერის დაწყება ვერ მოხერხდა" +Output.RecordFail.Unsupported="ამ სახის გამომავალი სიგნალი ან არაა მხარდაჭერილი, ან არ იძლევა ერთზე მეტი ხმოვანი ფაილის გამოყენების საშუალებას. გთხოვთ, გადაამოწმოთ თქვენი პარამეტრები და სცადოთ ხელახლა." +Output.RecordNoSpace.Title="არასაკმარისი ადგილი დისკზე" +Output.RecordNoSpace.Msg="დისკზე აღარაა საკმარისი ადგილი ჩაწერის გასაგრძელებლად." +Output.RecordError.Title="შეცდომა ჩაწერისას" +Output.RecordError.Msg="ჩაწერის დროს დაუდგენელი სახის შეცდომა წარმოიშვა." +Output.ReplayBuffer.NoHotkey.Title="სწრაფი ღილაკი არაა მითითებული!" +Output.ReplayBuffer.NoHotkey.Msg="სწრაფი ღილაკი არაა მითითებული გადახვევისთვის. გთხოვთ, მიუთითოთ „შენახვის“ ღილაკი, გადასახვევი მასალის შესანახად." +Output.BadPath.Title="ფაილის არამართებული მისამართი" +Output.BadPath.Text="ფაილის მითითებული მდებარეობა არასწორია. გთხოვთ, გადაამოწმოთ თქვენი პარამეტრების სისწორე." +LogReturnDialog="ჩანაწერი წარმატებით აიტვირთა" LogReturnDialog.CopyURL="ბმულის კოპირება" +LogReturnDialog.ErrorUploadingLog="შეცდომა აღრიცხვის ფაილის ატვირთვისას" +LicenseAgreement="სალიცენზიო შეთანხმება" +LicenseAgreement.PleaseReview="გთხოვთ, გაეცნოთ ლიცენზიის დადგენილებებს OBS-ით სარგებლობამდე. ამ პროგრამის გამოყენებით თქვენ ადასტურებთ, რომ წაიკითხეთ და ეთანხმებით GNU General Public v2.0 ლიცენზიის პირობებს. გთხოვთ, გადაადგილოთ გვერდი ქვემოთ, ხელშეკრულების სრულად სანახავად." +LicenseAgreement.ClickIAgreeToContinue="თუ თანახმა ხართ, მიიღოთ ხელშეკრულების პირობები, დააწკაპეთ ღილაკს „ვეთანხმები“. OBS-ით სარგებლობისთვის, აუცილებელია წინამდებარე პირობების მიღება." +LicenseAgreement.IAgree="ვეთანხმები" LicenseAgreement.Exit="გასვლა" +Remux.SourceFile="OBS ჩაწერა" +Remux.TargetFile="საბოლოო ფაილი" +Remux.Remux="ხელახლა მულტიპლექსირება" +Remux.OBSRecording="OBS ჩაწერა" +Remux.FinishedTitle="გარდაქმნა დასრულებულია" +Remux.Finished="ჩანაწერის გარდაქმნა დასრულდა" +Remux.FinishedError="ჩანაწერის გარდაქმნა დასრულდა, თუმცა ფაილი, შესაძლოა არასრული იყოს" +Remux.SelectRecording="ფაილის არჩევა OBS ჩანაწერისთვის…" +Remux.SelectTarget="საბოლოო ფაილის არჩევა …" +Remux.FileExistsTitle="საბოლოო ფაილი უკვე არჩეულია" +Remux.FileExists="საბოლოო ფაილი უკვე არჩეულია, გსურთ მისი შეცვლა?" +Remux.ExitUnfinishedTitle="მიმდინარეობს გარდაქმნა" +Remux.ExitUnfinished="გარდაქმნა ჯერ არ დასრულებულა, ახლავე შეწყვეტის შედეგად, დასამუშავებელი საბოლოო ფაილი გამოუსადეგარი გახდება.\nნამდვილად გსურთ გარდაქმნის შეწყვეტა?" +UpdateAvailable="განახლება ხელმისაწვდომია" +UpdateAvailable.Text="ხელმისაწვდომია ვერსია %1.%2.%3. დააწკაპეთ ჩამოსატვირთად" +Basic.DesktopDevice1="Desktop Audio" +Basic.DesktopDevice2="Desktop Audio 2" +Basic.AuxDevice1="Mic/Aux" Basic.AuxDevice2="Mic/Aux 2" Basic.AuxDevice3="Mic/Aux 3" Basic.AuxDevice4="Mic/Aux 4" +Basic.Scene="სცენა" +Basic.DisplayCapture="ეკრანის გადაღება" +Basic.Main.PreviewConextMenu.Enable="შეთვალიერების შესაძლებლობა" +ScaleFiltering="მასშტაბირების ფილტრი" +ScaleFiltering.Point="წერტილოვანი" +ScaleFiltering.Bilinear="ორხაზოვანი" +ScaleFiltering.Bicubic="ბიკუბური" +ScaleFiltering.Lanczos="Lanczos" +Deinterlacing="Deinterlacing" +Deinterlacing.Discard="გაუქმება" Deinterlacing.Retro="Retro" Deinterlacing.Blend="Blend" Deinterlacing.Blend2x="Blend 2x" +Deinterlacing.Linear="Linear" Deinterlacing.Linear2x="Linear 2x" Deinterlacing.Yadif="Yadif" Deinterlacing.Yadif2x="Yadif 2x" +Deinterlacing.TopFieldFirst="ზედა ველიდან" +Deinterlacing.BottomFieldFirst="ქვედა ველიდან" + +VolControl.SliderUnmuted="ხმის სამართავი '%1': %2" +VolControl.SliderMuted="ხმის სამართავი '%1': %2 (ამჟამად დადუმებული)" +VolControl.Mute="'%1' დადუმება" +VolControl.Properties="'%1' პარამეტრები" + +Basic.Main.AddSceneDlg.Title="სცენის დამატება" +Basic.Main.AddSceneDlg.Text="გთხოვთ, მიუთითოთ სცენის დასახელება" + +Basic.Main.DefaultSceneName.Text="სცენა %1" + +Basic.Main.AddSceneCollection.Title="სცენის კრებულის დამატება" +Basic.Main.AddSceneCollection.Text="გთხოვთ, მიუთითოთ სცენის კრებულის დასახელება" + +Basic.Main.RenameSceneCollection.Title="სცენის კრებულის გადარქმევა" + +AddProfile.Title="პროფილის დამატება" +AddProfile.Text="გთხოვთ, მიუთითოთ პროფილის დასახელება" + +RenameProfile.Title="პროფილის გადარქმევა" + +Basic.Main.MixerRename.Title="ხმის წყაროს გადარქმევა" +Basic.Main.MixerRename.Text="გთხოვთ, მიუთითოთ ხმის წყაროს დასახელება" +Basic.Main.PreviewDisabled="შეთვალიერება მიუწვდომელია" +Basic.SourceSelect="წყაროს შექმნა/მითითება" +Basic.SourceSelect.CreateNew="ახლის შექმნა" +Basic.SourceSelect.AddExisting="არსებულის დამატება" +Basic.SourceSelect.AddVisible="წყაროს გამოჩენა" +Basic.PropertiesWindow="'%1' პარამეტრები" +Basic.PropertiesWindow.AutoSelectFormat="%1 (თვითშერჩევა: %2)" +Basic.PropertiesWindow.SelectColor="ფერის შერჩევა" +Basic.PropertiesWindow.SelectFont="შრიფტის შერჩევა" +Basic.PropertiesWindow.ConfirmTitle="პარამეტრები შეცვლილია" +Basic.PropertiesWindow.Confirm="ცვლილებები არაა დამახსოვრებული. გსურთ მათი შენახვა?" +Basic.PropertiesWindow.NoProperties="პარამეტრები მიუწვდომელია" +Basic.PropertiesWindow.AddFiles="ფაილების დამატება" +Basic.PropertiesWindow.AddDir="საქაღალდის დამატება" +Basic.PropertiesWindow.AddURL="მისამართის/URL-ს დამატება" +Basic.PropertiesWindow.AddEditableListDir="საქაღალდის დამატება '%1'-ში" +Basic.PropertiesWindow.AddEditableListFiles="ფაილების დამატება '%1'" +Basic.PropertiesWindow.AddEditableListEntry="ელემენტის დამატება '%1'-ში" +Basic.PropertiesWindow.EditEditableListEntry="ელემენტის ჩასწორება '%1'-ში" +Basic.PropertiesView.FPS.Simple="ჩვეულებრივი FPS-მნიშვნელობები" +Basic.PropertiesView.FPS.Rational="რაციონალურ რიცხვიანი FPS-მნიშვნელობები" +Basic.PropertiesView.FPS.ValidFPSRanges="დაშვებული FPS-შუალედები:" +Basic.InteractionWindow="'%1'-თან ურთიერთქმედება" +Basic.StatusBar.Reconnecting="კავშირი გაწყვეტილია. ხელახლა დაკავშირება %2 წამში (%1 მცდელობა)" +Basic.StatusBar.AttemptingReconnect="ხელახლა დაკავშირება... (%1 მცდელობა)" +Basic.StatusBar.ReconnectSuccessful="ხელახლა დაუკავშირდა წარმატებით" +Basic.StatusBar.Delay="დაყოვნება (%1 წმ)" +Basic.StatusBar.DelayStartingIn="დაყოვნება (დაიწყება %1 წამში)" +Basic.StatusBar.DelayStoppingIn="დაყოვნება (შეწყდება %1 წამში)" +Basic.StatusBar.DelayStartingStoppingIn="დაყოვნება (შეწყდება %1 წამში, დაიწყება %2 წამში)" +Basic.Filters="ფილტრები" +Basic.Filters.AsyncFilters="ხმოვანი/ვიდეო ფილტრები" +Basic.Filters.AudioFilters="ხმოვანი ფილტრები" +Basic.Filters.EffectFilters="დასამუშავებელი ფილტრები" +Basic.Filters.Title="'%1' ფილტრები" +Basic.Filters.AddFilter.Title="ფილტრის დასახელება" +Basic.Filters.AddFilter.Text="გთხოვთ, მიუთითოთ ფილტრის დასახელება" - - - - - - - - +Basic.TransformWindow="სცენის ელემენტის გარდაქმნა" Basic.TransformWindow.Position="პოზიცია" +Basic.TransformWindow.Rotation="მობრუნება" +Basic.TransformWindow.Size="ზომა" +Basic.TransformWindow.Alignment="განლაგების გასწორება" +Basic.TransformWindow.BoundsType="შემომსაზღვრელი ჩარჩოს სახეები" +Basic.TransformWindow.BoundsAlignment="განლაგების გასწორება შემომსაზღვრელ ჩარჩოში" +Basic.TransformWindow.Bounds="შემომსაზღვრელი ჩარჩოს ზომა" +Basic.TransformWindow.Crop="შემოჭრა" +Basic.TransformWindow.Alignment.TopLeft="მარცხნივ ზემოთ" +Basic.TransformWindow.Alignment.TopCenter="შუაში ზემოთ" +Basic.TransformWindow.Alignment.TopRight="მარჯვნივ ზემოთ" +Basic.TransformWindow.Alignment.CenterLeft="მარცხნივ შუაში" +Basic.TransformWindow.Alignment.Center="შუაში" +Basic.TransformWindow.Alignment.CenterRight="მარჯვნივ შუაში" +Basic.TransformWindow.Alignment.BottomLeft="მარცხნივ ქვემოთ" +Basic.TransformWindow.Alignment.BottomCenter="შუაში ქვემოთ" +Basic.TransformWindow.Alignment.BottomRight="მარჯვნივ ქვემოთ" +Basic.TransformWindow.BoundsType.None="საზღვრების გარეშე" +Basic.TransformWindow.BoundsType.MaxOnly="მხოლოდ უმაღლესი ზომა" +Basic.TransformWindow.BoundsType.ScaleInner="ზომის მიყვანა შიდა საზღვრებამდე" +Basic.TransformWindow.BoundsType.ScaleOuter="ზომის მიყვანა გარე საზღვრებამდე" +Basic.TransformWindow.BoundsType.ScaleToWidth="ზომის დაყვანა საზღვრების სიგანემდე" +Basic.TransformWindow.BoundsType.ScaleToHeight="ზომის დაყვანა საზღვრების სიმაღლემდე" +Basic.TransformWindow.BoundsType.Stretch="გაწელვა საზღვრებამდე" +Basic.Main.AddSourceHelp.Title="წყარო ვერ დაემატება" +Basic.Main.AddSourceHelp.Text="საჭიროა, სულ მცირე 1 სცენა წყაროს დასამატებლად." +Basic.Main.Scenes="სცენები" +Basic.Main.Sources="წყაროები" +Basic.Main.Controls="სამართავი" +Basic.Main.Connecting="უკავშირდება..." +Basic.Main.StartRecording="ჩაწერის დაწყება" +Basic.Main.StartReplayBuffer="გადახვევის ჩართვა" +Basic.Main.StartStreaming="ნაკადის გაშვება" +Basic.Main.StopRecording="ნაკადის შეწყვეტა" +Basic.Main.StoppingRecording="ჩაწერის შეწყვეტა..." +Basic.Main.StopReplayBuffer="გადახვევის გამორთვა" +Basic.Main.StoppingReplayBuffer="გადახვევა გამოირთვება..." +Basic.Main.StopStreaming="ნაკადის შეწყვეტა" +Basic.Main.StoppingStreaming="ნაკადი წყდება..." +Basic.Main.ForceStopStreaming="ნაკადი წყდება (დაყოვნება უქმდება)" +Basic.Main.Group="ჯგუფი %1" +Basic.Main.GroupItems="შერჩეულების დაჯგუფება" +Basic.Main.Ungroup="განჯგუფება" +Basic.MainMenu.File="&ფაილი" +Basic.MainMenu.File.Export="&გატანა" +Basic.MainMenu.File.Import="&შემოტანა" +Basic.MainMenu.File.ShowRecordings="&ჩანაწერების ჩვენება" +Basic.MainMenu.File.Remux="ჩანაწერების ხელახლა მულტი&პლექსირება" +Basic.MainMenu.File.Settings="&პარამეტრები" +Basic.MainMenu.File.ShowSettingsFolder="პარამეტრების საქაღალდის ჩვენება" +Basic.MainMenu.File.ShowProfileFolder="პროფილის საქაღალდის ჩვენება" +Basic.MainMenu.AlwaysOnTop="&ყოველთვის წინა პლანზე" +Basic.MainMenu.File.Exit="&გამოსვლა" +Basic.MainMenu.Edit="&ჩასწორება" +Basic.MainMenu.Edit.Undo="&დაბრუნება" +Basic.MainMenu.Edit.Redo="&კვლავ შესრულება" +Basic.MainMenu.Edit.UndoAction="&დაბრუნება $1" +Basic.MainMenu.Edit.RedoAction="&კვლავ შესრულება $1" +Basic.MainMenu.Edit.LockPreview="შეთვალიერების &ჩაკეტვა" +Basic.MainMenu.Edit.Scale="შეთვალიერების &ზომის შეცვლა" +Basic.MainMenu.Edit.Scale.Window="ფანჯრის ზომამდე" +Basic.MainMenu.Edit.Scale.Canvas="ფონის ზომამდე (%1x%2)" +Basic.MainMenu.Edit.Scale.Output="გამომავალი ვიდეოს ზომამდე (%1x%2)" +Basic.MainMenu.Edit.Transform="&გარდაქმნა" +Basic.MainMenu.Edit.Transform.EditTransform="გარდაქმნის &ჩასწორება..." +Basic.MainMenu.Edit.Transform.CopyTransform="გარდაქმნის დაკოპირება" +Basic.MainMenu.Edit.Transform.PasteTransform="გარდაქმნის ჩასმა" +Basic.MainMenu.Edit.Transform.ResetTransform="&გარდაქმნის გაუქმება" +Basic.MainMenu.Edit.Transform.Rotate90CW="მობრუნება 90 გრადუსით საათის ისრის მიმართ." +Basic.MainMenu.Edit.Transform.Rotate90CCW="მობრუნება 90 გრადუსით საათის ისრის საწ. მიმართ." +Basic.MainMenu.Edit.Transform.Rotate180="მობრუნება 180 გრადუსით" +Basic.MainMenu.Edit.Transform.FlipHorizontal="&თარაზულად შეტრიალება" +Basic.MainMenu.Edit.Transform.FlipVertical="&შვეულად შეტრიალება" +Basic.MainMenu.Edit.Transform.FitToScreen="ეკრანის ზომაზე &მორგება" +Basic.MainMenu.Edit.Transform.StretchToScreen="ეკრანის ზომაზე &გაწელვა" +Basic.MainMenu.Edit.Transform.CenterToScreen="ეკრანის &შუაში განთავსება" +Basic.MainMenu.Edit.Order="&დალაგება" +Basic.MainMenu.Edit.Order.MoveUp="&ზემოთ აწევა" +Basic.MainMenu.Edit.Order.MoveDown="&ქვემოთ ჩამოწევა" +Basic.MainMenu.Edit.Order.MoveToTop="&თავში გადატანა" +Basic.MainMenu.Edit.Order.MoveToBottom="&ბოლოში გადატანა" +Basic.MainMenu.Edit.AdvAudio="ხმის &გაფართოებული პარამეტრები" +Basic.MainMenu.View="&ხედი" +Basic.MainMenu.View.Toolbars="&ხელსაწყოები" +Basic.MainMenu.View.Docks="იერსახის ნაწილები" Basic.MainMenu.View.Docks.ResetUI="UI-ს გადატვირთვა" +Basic.MainMenu.View.Docks.LockUI="UI-ს ჩაკეტვა" +Basic.MainMenu.View.Toolbars.Listboxes="&სიები" +Basic.MainMenu.View.SceneTransitions="ს&ცენებს შორის გადასვლები" +Basic.MainMenu.View.StatusBar="&მდგომარეობის ზოლი" +Basic.MainMenu.View.Fullscreen.Interface="სრულეკრანიანი" +Basic.MainMenu.SceneCollection="&სცენის კრებული" +Basic.MainMenu.Profile="&პროფილი" +Basic.MainMenu.Profile.Import="პროფილის შემოტანა" +Basic.MainMenu.Profile.Export="პროფილის შენახვა" +Basic.MainMenu.SceneCollection.Import="სცენის კრებულის შემოტანა" +Basic.MainMenu.SceneCollection.Export="სცენის კრებულის შენახვა" +Basic.MainMenu.Profile.Exists="ასეთი პროფილი უკვე არსებობს" +Basic.MainMenu.SceneCollection.Exists="ამ სცენის კრებული უკვე არსებობს" +Basic.MainMenu.Tools="&ხელსაწყოები" +Basic.MainMenu.Help="&დახმარება" +Basic.MainMenu.Help.HelpPortal="დახმარების &გვერდი" +Basic.MainMenu.Help.Website="ეწვიეთ &ვებსაიტს" +Basic.MainMenu.Help.Discord="&Discord სერვერზე შესვლა" +Basic.MainMenu.Help.Logs="&აღრიცხვის ფაილები" +Basic.MainMenu.Help.Logs.ShowLogs="აღრიცხვის ფაილების &ჩვენება" +Basic.MainMenu.Help.Logs.UploadCurrentLog="&მიმდინარე აღრიცხვის ფაილის ატვირთვა" +Basic.MainMenu.Help.Logs.UploadLastLog="&ბოლო აღრიცხვის ფაილის ატვირთვა" +Basic.MainMenu.Help.Logs.ViewCurrentLog="მიმდინარე აღრიცხვის ფაილის &ნახვა" +Basic.MainMenu.Help.CheckForUpdates="განახლებებზე შემოწმება" +Basic.MainMenu.Help.CrashLogs="ავარიული დახურვების &მოხსენებები" +Basic.MainMenu.Help.CrashLogs.ShowLogs="ავარიული დახურვების მოხსენებების &ჩვენება" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="&ბოლო მოხსენების ატვირთვა" +Basic.Settings.ProgramRestart="ამ ცვლილებების ასახვისთვის, საჭიროა პროგრამის ხელახლა გაშვება." +Basic.Settings.ConfirmTitle="ცვლილებების დადასტურება" +Basic.Settings.Confirm="ცვლილებები არაა დამახსოვრებული. გსურთ მათი შენახვა?" +Basic.Settings.General="მთავარი" +Basic.Settings.General.Theme="თემა" Basic.Settings.General.Language="ენა" +Basic.Settings.General.EnableAutoUpdates="განახლებებზე შემოწმება ჩართვისას" +Basic.Settings.General.OpenStatsOnStartup="სტატისტიკის ფანჯრის გახსნა ჩართვისას" +Basic.Settings.General.WarnBeforeStartingStream="დადასტურების ფანჯრის ჩვენება ნაკადის გაშვებისას" +Basic.Settings.General.WarnBeforeStoppingStream="დადასტურების ფანჯრის ჩვენება ნაკადის შეწყვეტისას" +Basic.Settings.General.Projectors="ჩვენებები" +Basic.Settings.General.HideProjectorCursor="მაჩვენებლის დამალვა ჩვენებებზე" +Basic.Settings.General.ProjectorAlwaysOnTop="ჩვენებების სხვა ფანჯრების ზემოთ დატოვება" +Basic.Settings.General.Snapping="წყაროს ჩარჩოს მიზიდვა განთავსებისას" +Basic.Settings.General.ScreenSnapping="წყაროს ჩარჩოს მიზიდვა ეკრანის კიდესთან" +Basic.Settings.General.CenterSnapping="წყაროს ჩარჩოს მიზიდვა შუაშია, თარაზულად და შვეულად" +Basic.Settings.General.SourceSnapping="წყაროს ჩარჩოს სხვა წყაროს ჩარჩოზე მიზიდვა" +Basic.Settings.General.SnapDistance="მიზიდვის ძალა" +Basic.Settings.General.RecordWhenStreaming="პირდაპირი ეთერის ავტომატური ჩაწერა" +Basic.Settings.General.KeepRecordingWhenStreamStops="ჩაწერის გაგრძელება ნაკადის შეწყვეტის შემდეგ" +Basic.Settings.General.ReplayBufferWhileStreaming="გადახვევის ჩართვა ავტომატურად ნაკადის გაშვებისას" +Basic.Settings.General.KeepReplayBufferStreamStops="გადახვევის შენარჩუნება, ნაკადის შეწყვეტისას" +Basic.Settings.General.SysTray="სისტემის არე" +Basic.Settings.General.SysTrayWhenStarted="სისტემის არეში ჩაკეცვა დაწყებისას" +Basic.Settings.General.SystemTrayHideMinimize="ყოველთვის სისტემის არეში ჩაკეცვა, ამოცანათა ზოლის ნაცვლად" +Basic.Settings.General.SaveProjectors="ჩვენებების დამახსოვრება გასვლისას" +Basic.Settings.General.SwitchOnDoubleClick="სცენაზე გადასვლა ორჯერ დაწკაპებისას" +Basic.Settings.General.StudioPortraitLayout="შვეული განლაგების ჩართვა" +Basic.Settings.General.Multiview="მრავალხედიანი ჩვენება" +Basic.Settings.General.Multiview.MouseSwitch="სცენებს შორის გადართვა დაწკაპებით" +Basic.Settings.General.Multiview.DrawSourceNames="სცენის სახელების ჩვენება" +Basic.Settings.General.Multiview.DrawSafeAreas="უსაფრთხო არეების გამოსახვა (EBU R 95)" +Basic.Settings.General.MultiviewLayout="მრავალხედიანი განლაგება" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="თარაზულად, ზემოთ (8 სცენა)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="თარაზულად, ქვემოთ (8 სცენა)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="შვეულად, მარცხნივ (8 სცენა)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="შვეულად, მარჯვნივ (8 სცენა)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="თარაზულად, ზემოთ (24 სცენა)" +Basic.Settings.Stream="ნაკადი" +Basic.Settings.Stream.StreamType="ნაკადის სახეობა" +Basic.Settings.Output="გამომავალი სიგნალი" +Basic.Settings.Output.Format="ჩაწერის ფორმატი" +Basic.Settings.Output.Encoder="დამშიფრავი" +Basic.Settings.Output.SelectDirectory="საქაღალდის არჩევა ჩანაწერისთვის" +Basic.Settings.Output.SelectFile="ფაილის არჩევა ჩანაწერისთვის" +Basic.Settings.Output.EnforceBitrate="ნაკადის გაშვების მომსახურების ბიტური სიხშირის ზღვრების დადგენა" +Basic.Settings.Output.Mode="გამომავალი სიგნალის რეჟიმი" +Basic.Settings.Output.Mode.Simple="მარტივი" +Basic.Settings.Output.Mode.Adv="გაფართოებული" +Basic.Settings.Output.Mode.FFmpeg="FFmpeg გამომავალი სიგნალი" +Basic.Settings.Output.UseReplayBuffer="გადახვევის შესაძლებლობა" +Basic.Settings.Output.ReplayBuffer.SecondsMax="გადახვევის დასაშვები დრო (წამი)" +Basic.Settings.Output.ReplayBuffer.MegabytesMax="მეხსიერების დასაშვები მოცულობა (მეგაბაიტებში)" +Basic.Settings.Output.ReplayBuffer.Estimate="მეხსიერების მიახლოებითი მოხმარება: %1 MB" +Basic.Settings.Output.ReplayBuffer.EstimateUnknown="შეუძლებელია მეხსიერების მიახლოებითი მოხმარების დადგენა. გთხოვთ, მიუთითოთ მეხსიერების დასაშვები ზღვარი." +Basic.Settings.Output.ReplayBuffer.HotkeyMessage="(შენიშვნა: დარწმუნდით, რომ გადახვევისთვის სწრაფი ღილაკები დაყენებულია შესაბამის განყოფილებაში)" +Basic.Settings.Output.ReplayBuffer.Prefix="გადასახვევი მასალის შესანახი ფაილის თავსართი" +Basic.Settings.Output.ReplayBuffer.Suffix="ბოლოსართი" +Basic.Settings.Output.Simple.SavePath="ჩანაწერის მდებარეობა" +Basic.Settings.Output.Simple.RecordingQuality="ჩაწერის ხარისხი" +Basic.Settings.Output.Simple.RecordingQuality.Stream="გაშვებული ნაკადის შესაბამისი" +Basic.Settings.Output.Simple.RecordingQuality.Small="მაღალი ხარისხი, საშუალო ზომის ფაილი" +Basic.Settings.Output.Simple.RecordingQuality.HQ="განუსაზღვრელი ხარისხი, დიდი ზომის ფაილი" +Basic.Settings.Output.Simple.RecordingQuality.Lossless="უდანაკარგო ხარისხი, მეტისმეტად დიდი ზომის ფაილი" +Basic.Settings.Output.Simple.Warn.VideoBitrate="გაფრთხილება: ვიდეონაკადის ბიტურ სიხშირედ მიეთითება %1, რაც წარმოადგენს უმაღლეს დაშვებულ ზღვარს, ნაკადის გაშვების მოცემული მომსახურებისთვის. თუ ნამდვილად გსურთ %1 ბიტურ სიხშირეზე მეტის მიღება, ჩართეთ დაშიფვრის გაფართოებული პარამეტრები და მოხსენით მონიშვნა \"ნაკადის გაშვების მომსახურების ბიტური სიხშირის ზღვრების დადგენას\"." +Basic.Settings.Output.Simple.Warn.AudioBitrate="გაფრთხილება: ხმოვანი ნაკადის ბიტურ სიხშირედ მიეთითება %1, რაც წარმოადგენს უმაღლეს დაშვებულ ზღვარს, ნაკადის გაშვების მოცემული მომსახურებისთვის. თუ ნამდვილად გსურთ %1 ბიტურ სიხშირეზე მეტის მიღება, ჩართეთ დაშიფვრის გაფართოებული პარამეტრები და მოხსენით მონიშვნა \"ნაკადის გაშვების მომსახურების ბიტური სიხშირის ზღვრების დადგენას\"." +Basic.Settings.Output.Simple.Warn.Encoder="გაფრთხილება: გაშვებული ნაკადისგან განსხვავებულ ხარისხში ჩანაწერის დაშიფვრა, ზრდის პროცესორის დატვირთვას, როცა ნაკადის გაშვება და ჩაწერა, ერთდროულად მიმდინარეობს." +Basic.Settings.Output.Simple.Warn.Lossless="გაფრთხილება: უდანაკარგო ხარისხის მითითების შემთხვევაში, შეიქმნება მეტისმეტად დიდი ზომის ფაილები! უდანაკარგო ხარისხის ვიდეოს თითოეული წუთის მოცულობამ დისკზე, შესაძლოა 7 გიგაბაიტს გადააჭარბოს, მაღალი გარჩევადობისა და კადრის სიხშირის პირობებში. ხანგრძლივი ჩანაწერებისთვის, უდანაკარგოს არჩევა არაა მიზანშეწონილი, თუ არ გაქვთ საკმარისად დიდი მოცულობის თავისუფალი ადგილი დისკზე." +Basic.Settings.Output.Simple.Warn.Lossless.Msg="ნამდვილად გსურთ უდანაკარგო ხარისხის მითითება?" +Basic.Settings.Output.Simple.Warn.Lossless.Title="გაფრთხილება, უდანაკარგო ხარისხის შესახებ!" +Basic.Settings.Output.Simple.Warn.MultipleQSV="გაფრთხილება: ერთდროულად რამდენიმე სხვადასხვა QSV დამშიფრავის გამოყენება ნაკადის გაშვებისას და ჩაწერისას, შეუძლებელია. თუ გსურთ ნაკადის გაშვება და ჩაწერა ერთდროულად, გთხოვთ შეცვალოთ ან ჩაწერის დამშიფრავი, ან ნაკადის დამშიფრავი." +Basic.Settings.Output.Simple.Encoder.Software="პროგრამული (x264)" +Basic.Settings.Output.Simple.Encoder.Hardware.QSV="აპარატურული (QSV)" +Basic.Settings.Output.Simple.Encoder.Hardware.AMD="აპარატურული (AMD)" +Basic.Settings.Output.Simple.Encoder.Hardware.NVENC="აპარატურული (NVENC)" +Basic.Settings.Output.Simple.Encoder.SoftwareLowCPU="პროგრამული (x264 პროცესორის დაბალი მოხმარების მზა პარამეტრები, ზრდის ფაილის ზომას)" +Basic.Settings.Output.VideoBitrate="ვიდეოს ბიტური სიხშირე" +Basic.Settings.Output.AudioBitrate="ხმის ბიტური სიხშირე" +Basic.Settings.Output.Reconnect="ხელახლა დაკავშირება ავტომატურად" +Basic.Settings.Output.RetryDelay="გამეორების დაყოვნება (წამი)" +Basic.Settings.Output.MaxRetries="გამეორების დასაშვები რაოდენობა" +Basic.Settings.Output.Advanced="დამშიფრავის გაფართოებული პარამეტრების ჩართვა" +Basic.Settings.Output.EncoderPreset="დაშიფვრის მზა პარამეტრები (მაღალი = ნაკლები CPU-დატვირთვა)" +Basic.Settings.Output.CustomEncoderSettings="დამშიფრავის პარამეტრების მითითება" +Basic.Settings.Output.CustomMuxerSettings="მულტიპლექსორის მითითებული პარამეტრები" +Basic.Settings.Output.NoSpaceFileName="ფაილის სახელის შექმნა, გამოტოვებული ადგილების გარეშე" +Basic.Settings.Output.Adv.Rescale="გამომავალი ვიდეოს ზომების შეცვლა" +Basic.Settings.Output.Adv.AudioTrack="ხმოვანი ბილიკი" +Basic.Settings.Output.Adv.Streaming="ნაკადი" +Basic.Settings.Output.Adv.ApplyServiceSettings="ნაკადის გაშვების მომსახურების პარამეტრების გამოყენება" +Basic.Settings.Output.Adv.Audio.Track1="ბილიკი 1" +Basic.Settings.Output.Adv.Audio.Track2="ბილიკი 2" +Basic.Settings.Output.Adv.Audio.Track3="ბილიკი 3" +Basic.Settings.Output.Adv.Audio.Track4="ბილიკი 4" +Basic.Settings.Output.Adv.Audio.Track5="ბილიკი 5" +Basic.Settings.Output.Adv.Audio.Track6="ბილიკი 6" +Basic.Settings.Output.Adv.Recording="ჩაწერა" Basic.Settings.Output.Adv.Recording.Type="ტიპი" Basic.Settings.Output.Adv.Recording.Type.Standard="სტანდარტული" +Basic.Settings.Output.Adv.Recording.Type.FFmpegOutput="მითითებული გამომავალი სიგნალი (FFmpeg)" +Basic.Settings.Output.Adv.Recording.UseStreamEncoder="(გაშვებული ნაკადის დამშიფრავის გამოყენება)" +Basic.Settings.Output.Adv.Recording.Filename="ფაილის დასახელების ფორმატი" +Basic.Settings.Output.Adv.Recording.OverwriteIfExists="არსებულ ფაილზე გადაწერა" +Basic.Settings.Output.Adv.FFmpeg.Type="FFmpeg გამომავალი სიგნალის სახე" +Basic.Settings.Output.Adv.FFmpeg.Type.URL="მითითებულ URL ბულზე" +Basic.Settings.Output.Adv.FFmpeg.Type.RecordToFile="ფაილში" +Basic.Settings.Output.Adv.FFmpeg.SaveFilter.Common="ჩაწერის ცნობილი ფორმატები" Basic.Settings.Output.Adv.FFmpeg.SaveFilter.All="ყველა ფაილი" +Basic.Settings.Output.Adv.FFmpeg.SavePathURL="ფაილის მისამართი ან URL" +Basic.Settings.Output.Adv.FFmpeg.Format="სათავსის ფორმატი" +Basic.Settings.Output.Adv.FFmpeg.FormatAudio="ხმა" Basic.Settings.Output.Adv.FFmpeg.FormatVideo="ვიდეო" +Basic.Settings.Output.Adv.FFmpeg.FormatDefault="ნაგულისხმევი ფორმატი" +Basic.Settings.Output.Adv.FFmpeg.FormatDesc="სათავსის ფორმატის აღწერა" +Basic.Settings.Output.Adv.FFmpeg.FormatDescDef="ხმის/ვიდეოს კოდეკის ამოცნობა მითითებული ფაილის მისამართიდან ან URL-დან" +Basic.Settings.Output.Adv.FFmpeg.AVEncoderDefault="ნაგულისხმევი დამშიფრავი" +Basic.Settings.Output.Adv.FFmpeg.AVEncoderDisable="დამშიფრავის გამორთვა" +Basic.Settings.Output.Adv.FFmpeg.VEncoder="ვიდეოს დამშიფრავი" +Basic.Settings.Output.Adv.FFmpeg.VEncoderSettings="ვიდეოს დამშიფრავის პარამეტრები (თუ არის)" +Basic.Settings.Output.Adv.FFmpeg.AEncoder="ხმის დამშიფრავი" +Basic.Settings.Output.Adv.FFmpeg.AEncoderSettings="ხმის დამშიფრავის პარამეტრები (თუ არის)" +Basic.Settings.Output.Adv.FFmpeg.MuxerSettings="მულტიპლექსორის პარამეტრები (თუ არის)" +Basic.Settings.Output.Adv.FFmpeg.GOPSize="საკვანძო კადრებს შორის შუალედი (კადრები)" +Basic.Settings.Output.Adv.FFmpeg.IgnoreCodecCompat="ყველა კოდეკის ჩვენება (მათ შორის არათავსებადებისაც)" +FilenameFormatting.completer="%CCYY-%MM-%DD %hh-%mm-%ss\n%YY-%MM-%DD %hh-%mm-%ss\n%Y-%m-%d %H-%M-%S\n%y-%m-%d %H-%M-%S\n%a %Y-%m-%d %H-%M-%S\n%A %Y-%m-%d %H-%M-%S\n%Y-%b-%d %H-%M-%S\n%Y-%B-%d %H-%M-%S\n%Y-%m-%d %I-%M-%S-%p\n%Y-%m-%d %H-%M-%S-%z\n%Y-%m-%d %H-%M-%S-%Z" +FilenameFormatting.TT="%CCYY წელი, ოთხი ციფრი\n%YY წელი, ბოლო ორის ციფრი(00-99)\n%MM თვე, ათობითი რიცხვის სახით (01-12)\n%DD თვის რიცხვი, წინ ნულით (01-31)\n%hh საათი, 24-საათიანი ფორმატით (00-23)\n%mm წუთი(00-59)\n%ss წამი(00-61)\n%% A % ნიშანი\n%a კვირის დღე შემოკლებულად\n%A კვირის დღის სრული დასახელება\n%b თვე შემოკლებულად\n%B თვის სრული დასახელება\n%d თვის რიცხვი, წინ ნულით (01-31)\n%H საათი, 24-საათიანი ფორმატით (00-23)\n%I საათი, 12-საათიანი ფორმატით (01-12)\n%m თვე, ათობითი რიცხვის სახით (01-12)\n%M წუთი (00-59)\n%p AM ან PM აღნიშვნა\n%S წამი (00-61)\n%y წელი, ბოლო ორი ციფრი (00-99)\n%Y წელი\n%z ISO 8601 სხვაობა UTC დროსთან ან სასაათე სარტყელთან\n სრული ან შემოკლებული დასახელება\n%Z სასაათე სარტყელი ან მისი შემოკლებული დასახელება\n" +Basic.Settings.Video="ვიდეო" +Basic.Settings.Video.Adapter="ვიდეოდაფა" +Basic.Settings.Video.BaseResolution="ეკრანის (ფონის) ძირითადი გაფართოება" +Basic.Settings.Video.ScaledResolution="გამომავალი ვიდეოს (მასშტაბირებულის) გაფართოება" +Basic.Settings.Video.DownscaleFilter="მასშტაბირების ფილტრი" +Basic.Settings.Video.DisableAeroWindows="Aero გაფორმების გათიშვა (მხოლოდ Windows-ზე)" Basic.Settings.Video.FPS="FPS" +Basic.Settings.Video.FPSCommon="საერთო FPS-მნიშვნელობები" +Basic.Settings.Video.FPSInteger="მთელრიცხვიანი FPS-მნიშვნელობები" +Basic.Settings.Video.FPSFraction="წილადიანი FPS-მნიშვნელობები" +Basic.Settings.Video.Numerator="მრიცხველი" Basic.Settings.Video.Denominator="მნიშვნელი" +Basic.Settings.Video.Renderer="დამმუშავებელი" +Basic.Settings.Video.InvalidResolution="გაფართოების არასწორი მნიშვნელობა. უნდა იყოს [width]x[height] (მაგ. 1920x1080)" +Basic.Settings.Video.CurrentlyActive="ვიდეო ამჟამად გაშვებულია. გთხოვთ, გამორთოთ ყველა გამომავალი სიგნალი, ვიდეოს პარამეტრების ჩასასწორებლად." +Basic.Settings.Video.DisableAero="Aero გაფორმების გათიშვა" +Basic.Settings.Video.DownscaleFilter.Bilinear="ორხაზოვანი (უსწრაფესი, მაგრამ ბუნდოვანი მასშტაბირება)" +Basic.Settings.Video.DownscaleFilter.Bicubic="ბიკუბური (მკვეთრი მასშტაბირება, 16 შერჩევა)" +Basic.Settings.Video.DownscaleFilter.Lanczos="Lanczos (მკვეთრი მასშტაბირება, 32 შერჩევა)" +Basic.Settings.Audio="ხმა" +Basic.Settings.Audio.SampleRate="სიხშირე" Basic.Settings.Audio.Channels="არხები" +Basic.Settings.Audio.MeterDecayRate="ხმის ზოლის დაშვების სიჩქარე" +Basic.Settings.Audio.MeterDecayRate.Fast="სწრაფი" +Basic.Settings.Audio.MeterDecayRate.Medium="საშუალო (I სახის PPM)" +Basic.Settings.Audio.MeterDecayRate.Slow="ნელი (II სახის PPM)" +Basic.Settings.Audio.PeakMeterType="ხმის სიმაღლის მზომის სახეები" +Basic.Settings.Audio.PeakMeterType.SamplePeak="უბრალო" +Basic.Settings.Audio.PeakMeterType.TruePeak="ზუსტი (პროცესორის მაღალი მოხმარებით)" +Basic.Settings.Audio.MultiChannelWarning.Enabled="ყურადღება: ჩართულია მოცულობითი ხმა." +Basic.Settings.Audio.MultichannelWarning="ნაკადის გაშვებისას, გადაამოწმეთ მომსახურების მომწოდებელთან, არის თუ არა მხარდაჭერილი ორივე, მოცულობითი ხმოვანი სიგნალის მიღებაც და მოსმენაც. მაგალითად Twitch, Facebook 360 Live, Mixer RTMP, Smashcast შემთხვევებში, მოცულობითი ხმოვანი სიგნალი, სრულადაა მხარდაჭერილი. მიუხედავად იმისა, რომ Facebook Live და YouTube Live, ორივე იღებს მოცულობით ხმოვან სიგნალს, Facebook Live გარდაქმნის მას სტერეო-სიგნალად, ხოლო YouTube Live უშვებს მხოლოდ ორი არხით.\n\nOBS-ის ხმოვანი ფილტრები თავსებადია მოცულობით ხმოვან სიგნალთან, მაგრამ VST მოდულის მხარდაჭერა, შესაძლოა არ იყოს უზრუნველყოფილი." +Basic.Settings.Audio.MultichannelWarning.Title="ჩაირთოს მოცულობითი ხმოვანი სიგნალი?" +Basic.Settings.Audio.MultichannelWarning.Confirm="ნამდვილად გსურთ, ჩართოთ მოცულობითი ხმოვანი სიგნალი?" +Basic.Settings.Audio.DesktopDevice="ხმის მოწყობილობა" +Basic.Settings.Audio.DesktopDevice2="ხმის მოწყობილობა 2" +Basic.Settings.Audio.AuxDevice="მიკროფონი/ხმის დამატებითი მოწყობილობა" +Basic.Settings.Audio.AuxDevice2="მიკროფონი/ხმის დამატებითი მოწყობილობა 2" +Basic.Settings.Audio.AuxDevice3="მიკროფონი/ხმის დამატებითი მოწყობილობა 3" +Basic.Settings.Audio.EnablePushToMute="დაჭერით დადუმების ჩართვა" +Basic.Settings.Audio.PushToMuteDelay="დაჭერით დადუმების დაყოვნება" +Basic.Settings.Audio.EnablePushToTalk="დაჭერით საუბრის ჩართვა" +Basic.Settings.Audio.PushToTalkDelay="დაჭერით საუბრის დაყოვნება" +Basic.Settings.Audio.UnknownAudioDevice="[მოწყობილობა არაა დაკავშირებული ან მიუწვდომელია]" +Basic.Settings.Advanced="დამატებითი" +Basic.Settings.Advanced.General.ProcessPriority="უპირატესობა დამუშავებისას" +Basic.Settings.Advanced.General.ProcessPriority.High="მაღალი" +Basic.Settings.Advanced.General.ProcessPriority.AboveNormal="საშუალოზე მაღალი" +Basic.Settings.Advanced.General.ProcessPriority.Normal="საშუალო" +Basic.Settings.Advanced.General.ProcessPriority.BelowNormal="საშუალოზე დაბალი" +Basic.Settings.Advanced.General.ProcessPriority.Idle="უმოქმედო" +Basic.Settings.Advanced.FormatWarning="გაფრთხილება: ფერთა ფორმატები, გარდა NV12-ისა, ძირითადად განკუთვნილია ჩაწერისთვის და არა ნაკადის გაშვებისთვის. ნაკადის გაშვებისას, ფერთა ფორმატის გარდაქმნებმა შესაძლოა გამოიწვიოს პროცესორის მეტად დატვირთვა." +Basic.Settings.Advanced.Audio.BufferingTime="ხმის ბუფერის დრო" +Basic.Settings.Advanced.Video.ColorFormat="ფერთა ფორმატი" +Basic.Settings.Advanced.Video.ColorSpace="YUV ფერთა სისტემა" +Basic.Settings.Advanced.Video.ColorRange="YUV ფერთა გამა" +Basic.Settings.Advanced.Video.ColorRange.Partial="ნაწილობრივი" +Basic.Settings.Advanced.Video.ColorRange.Full="სრული" +Basic.Settings.Advanced.Audio.MonitoringDevice="ხმის მოსასმენი მოწყობილობა" +Basic.Settings.Advanced.Audio.MonitoringDevice.Default="ნაგულისხმევი" +Basic.Settings.Advanced.Audio.DisableAudioDucking="Windows-ის მიერ ხმის დონის დადაბლების არიდება" +Basic.Settings.Advanced.StreamDelay="ნაკადის დაყოვნება" +Basic.Settings.Advanced.StreamDelay.Duration="ხანგრძლივობა (წამი)" +Basic.Settings.Advanced.StreamDelay.Preserve="მოსაჭრელი წერტილის მომარაგება (დაყოვნების გაზრდა) ხელახლა დაკავშირებისას" +Basic.Settings.Advanced.StreamDelay.MemoryUsage="მეხსიერების მიახლოებითი მოხმარება: %1 MB" +Basic.Settings.Advanced.Network="ქსელი" +Basic.Settings.Advanced.Network.BindToIP="დაკავშირება IP მისამართზე" +Basic.Settings.Advanced.Network.EnableNewSocketLoop="ახალი ქსელის კოდის ჩართვა" +Basic.Settings.Advanced.Network.EnableLowLatencyMode="დაყოვნების შემცირება" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="სწრაფი ღილაკების გათიშვა, მთავარ ფანჯარაზე გადასვლისას" +Basic.AdvAudio="ხმის გაფართოებული პარამეტრები" Basic.AdvAudio.Name="სახელი" +Basic.AdvAudio.Volume="ხმის სიმაღლე (%)" +Basic.AdvAudio.Mono="ერთ არხში გაერთიანება" +Basic.AdvAudio.Panning="წონასწორობა" +Basic.AdvAudio.SyncOffset="სინქრონიზაციის გასწორება (მწ)" +Basic.AdvAudio.Monitoring="ხმის მოსმენა" +Basic.AdvAudio.Monitoring.None="მოსმენის გარეშე" +Basic.AdvAudio.Monitoring.MonitorOnly="მხოლოდ მოსმენა (უხმო გამომავალი სიგნალით)" +Basic.AdvAudio.Monitoring.Both="მოსმენა და გამოტანა" +Basic.AdvAudio.AudioTracks="ბილიკები" +Basic.Settings.Hotkeys="სწრაფი ღილაკები" +Basic.Settings.Hotkeys.Pair="'%1' - ღილაკების ერთობლიობა იმუშავებს გადამრთველის სახით" +Basic.Hotkeys.SelectScene="სცენაზე გადასვლა" +Basic.SystemTray.Show="ჩვენება" +Basic.SystemTray.Hide="დამალვა" +Basic.SystemTray.Message.Reconnecting="კავშირი გაწყდა. უკავშირდება ხელახლა..." +Hotkeys.Insert="Insert" +Hotkeys.Delete="Delete" +Hotkeys.Home="Home" +Hotkeys.End="End" +Hotkeys.PageUp="Page Up" +Hotkeys.PageDown="Page Down" +Hotkeys.NumLock="Num Lock" +Hotkeys.ScrollLock="Scroll Lock" Hotkeys.CapsLock="Caps Lock" Hotkeys.Backspace="Backspace" +Hotkeys.Tab="Tab" +Hotkeys.Print="Print" Hotkeys.Pause="პაუზა" +Hotkeys.Left="მარცხენა ისარი" +Hotkeys.Right="მარჯვენა ისარი" +Hotkeys.Up="ზედა ისარი" +Hotkeys.Down="ქვედა ისარი" +Hotkeys.Windows="Windows" Hotkeys.Super="Super" Hotkeys.Menu="მენიუ" Hotkeys.Space="Space" Hotkeys.NumpadNum="Numpad %1" +Hotkeys.NumpadMultiply="გამრავლება ციფრების დაფაზე" +Hotkeys.NumpadDivide="გაყოფა ციფრების დაფაზე" +Hotkeys.NumpadAdd="მიმატება ციფრების დაფაზე" +Hotkeys.NumpadSubtract="გამოკლება ციფრების დაფაზე" +Hotkeys.NumpadDecimal="ათწილადის ნიშანი ციფრების დაფაზე" +Hotkeys.AppleKeypadNum="%1 (კლავიატურაზე)" +Hotkeys.AppleKeypadMultiply="* (კლავიატურაზე)" +Hotkeys.AppleKeypadDivide="/ (კლავიატურაზე)" +Hotkeys.AppleKeypadAdd="+ (კლავიატურაზე)" +Hotkeys.AppleKeypadSubtract="- (კლავიატურაზე)" +Hotkeys.AppleKeypadDecimal=". (კლავიატურაზე)" +Hotkeys.AppleKeypadEqual="= (კლავიატურაზე)" +Hotkeys.MouseButton="თაგვი %1" +Mute="დადუმება" +Unmute="ხმის ჩართვა" +Push-to-mute="დაჭერისას დადუმება" +Push-to-talk="დაჭერისას საუბარი" +SceneItemShow="'%1'-ის გამოჩენა" +SceneItemHide="'%1'-ის დამალვა" +OutputWarnings.NoTracksSelected="უნდა მიუთითოთ ერთი ხმოვანი ბილიკი მაინც" +OutputWarnings.MultiTrackRecording="გაფრთხილება: ცალკეული სახის ფაილებში (როგორიცაა FLV), არაა მხარდაჭერილი რამდენიმე ბილიკი, თითოეული ჩაწერისას" +OutputWarnings.MP4Recording="გაფრთხილება: MP4 სახით შენახული ჩანაწერები ვეღარ აღდგება, მუშაობის შეწყვეტის შემთხვევაში (მაგ. ლურჯი ეკრანის ამოგდებისას, ძაბვის ვარდნისას და ა.შ.). თუ გსურთ რამდენიმე ხმოვანი ფაილის ჩაწერა, სასურველია ამისთვის გამოიყენოთ MKV და დასრულების შემდეგ გარდაქმნათ MP4-ად. (ფაილი->ჩანაწერების გარდაქმნა)" +FinalScene.Title="სცენის წაშლა" +FinalScene.Text="საჭიროებს, სულ მცირე ერთ სცენას." + +NoSources.Title="წყაროები არაა" +NoSources.Text="როგორც ჩანს, თქვენ ჯერ არ დაგიმატებიათ ვიდეოს არცერთი წყარო, შედეგად მიიღებთ ცარიელ ეკრანს. ნამდვილად გსურთ, განაგრძოთ?" +NoSources.Text.AddSource="წყაროს დამატება შეგიძლიათ ნებისმიერ დროს + ნიშანზე დაწკაპებით, მთავარი ფანჯრის წყაროების არეში." + +ChangeBG="ფერის დაყენება" +CustomColor="ფერის მითითება" + +BrowserSource.EnableHardwareAcceleration="ბრაუზერის აპარატურული აჩქარების ჩართვა" diff --git a/UI/data/locale/ko-KR.ini b/UI/data/locale/ko-KR.ini index fb684d8..4abb6a2 100644 --- a/UI/data/locale/ko-KR.ini +++ b/UI/data/locale/ko-KR.ini @@ -78,6 +78,8 @@ None="없음" StudioMode.Preview="미리보기" StudioMode.Program="프로그램" ShowInMultiview="다중화면으로 표시" +VerticalLayout="수직으로 배치" +Group="하나로 묶기" AlreadyRunning.Title="OBS가 이미 실행 중입니다" AlreadyRunning.Text="OBS가 이미 실행 중입니다! 의도한 것이 아니라면 새로운 OBS를 실행하기 전에 이미 동작 중인 프로그램을 종료하십시오. OBS가 시스템 트레이에 최소화되어 있는지도 확인하십시오." @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="리플레이 버퍼를 멈추고 있습니다.. Basic.Main.StopStreaming="방송 중단" Basic.Main.StoppingStreaming="방송을 중지합니다..." Basic.Main.ForceStopStreaming="방송 중지 (지연된 분량도 마무리없이 즉시 송출 중단)" +Basic.Main.Group="묶음: %1" +Basic.Main.GroupItems="선택한 항목 묶기" +Basic.Main.Ungroup="묶음 해체" Basic.MainMenu.File="파일(&F)" Basic.MainMenu.File.Export="내보내기(&E)" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="도구(&T)" Basic.MainMenu.Help="도움말(&H)" Basic.MainMenu.Help.HelpPortal="도움말 및 포털" Basic.MainMenu.Help.Website="웹사이트 방문(&W)" +Basic.MainMenu.Help.Discord="공식 디스코드 참여(&D)" Basic.MainMenu.Help.Logs="기록 파일(&L)" Basic.MainMenu.Help.Logs.ShowLogs="기록 파일 표시(S)" Basic.MainMenu.Help.Logs.UploadCurrentLog="현재 기록 파일 올리기(&C)" Basic.MainMenu.Help.Logs.UploadLastLog="마지막 기록 파일 올리기(&L)" Basic.MainMenu.Help.Logs.ViewCurrentLog="현재 기록 보기(&V)" Basic.MainMenu.Help.CheckForUpdates="판올림 확인" +Basic.MainMenu.Help.CrashLogs="오류 보고(&R)" +Basic.MainMenu.Help.CrashLogs.ShowLogs="오류 보고서 보기(&S)" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="마지막 오류 보고서 전송하기(&L)" Basic.Settings.ProgramRestart="설정을 적용하려면 프로그램을 다시 시작해야 합니다." Basic.Settings.ConfirmTitle="변경사항 확인" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="작업 표시줄 대신 시스템 Basic.Settings.General.SaveProjectors="종료 시 프로젝터 저장" Basic.Settings.General.SwitchOnDoubleClick="더블클릭 시 장면으로 전환" Basic.Settings.General.StudioPortraitLayout="프로젝터를 수직으로 배열" +Basic.Settings.General.Multiview="다중화면" +Basic.Settings.General.Multiview.MouseSwitch="클릭으로 장면 간 전환" +Basic.Settings.General.Multiview.DrawSourceNames="장면 이름 표시" +Basic.Settings.General.Multiview.DrawSafeAreas="안전 영역 표시 (EBU R 95)" Basic.Settings.General.MultiviewLayout="다중화면 배치" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="수평, 위" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="수평, 아래" -Basic.Settings.General.MultiviewLayout.Vertical.Left="수직, 왼쪽" -Basic.Settings.General.MultiviewLayout.Vertical.Right="수직, 오른쪽" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="수평, 상단 (8 장면)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="수평, 하단 (8장면)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="수직, 좌측 (8장면)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="수직, 우측 (8장면)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="수평, 상단 (24 장면)" Basic.Settings.Stream="방송" Basic.Settings.Stream.StreamType="방송 형식" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="오디오 측정기 감퇴 속도" Basic.Settings.Audio.MeterDecayRate.Fast="빠름" Basic.Settings.Audio.MeterDecayRate.Medium="중간 (Type I PPM)" Basic.Settings.Audio.MeterDecayRate.Slow="느림 (Type II PPM)" +Basic.Settings.Audio.PeakMeterType="피크 미터 형식" +Basic.Settings.Audio.PeakMeterType.SamplePeak="샘플 피크" +Basic.Settings.Audio.PeakMeterType.TruePeak="트루 피크 (더 높은 CPU 자원 요구)" Basic.Settings.Audio.MultiChannelWarning.Enabled="경고: 서라운드 음향이 켜져 있습니다." Basic.Settings.Audio.MultichannelWarning="방송 중이라면 서비스에서 서라운드 음향에 대한 입력 및 재생을 지원하는지 확인하세요. 트위치, 페이스북 360 라이브, Mixer RTMP, Smashcast 는 해당 기능을 사용할 수 있습니다. 페이스북 Live와 유튜브 Live 서비스는 입력은 할 수 있지만 스테레오로 전환하며 유튜브 Live는 오로지 2채널로만 재생합니다.\n\nOBS 오디오 필터는 서라운드 음향을 지원하지만 VST 플러그인 지원은 보장하지 않습니다." Basic.Settings.Audio.MultichannelWarning.Title="서라운드 음향을 활성화할까요?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="네트워크" Basic.Settings.Advanced.Network.BindToIP="IP에 고정" Basic.Settings.Advanced.Network.EnableNewSocketLoop="새로운 네트워크 코드 활성화" Basic.Settings.Advanced.Network.EnableLowLatencyMode="짧은 지연시간 방식 사용" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="주요 창을 초점으로 둘 때 단축키를 비활성화" Basic.AdvAudio="오디오 고급 설정" Basic.AdvAudio.Name="이름" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="경고: MP4로 녹화를 하면 파일이 마무리 FinalScene.Title="장면 삭제" FinalScene.Text="적어도 하나의 장면은 존재해야 합니다." +NoSources.Title="소스 없음" +NoSources.Text="어떠한 영상 소스도 추가하지 않아서 빈 화면만 송출할 것입니다. 그래도 계속하시겠습니까?" +NoSources.Text.AddSource="주 화면 소스 목록에 있는 + 아이콘을 누르면 소스를 추가할 수 있습니다." + +ChangeBG="색상 지정" +CustomColor="사용자 색상" + +BrowserSource.EnableHardwareAcceleration="브라우저 소스에 하드웨어 가속 활성화" + diff --git a/UI/data/locale/lt-LT.ini b/UI/data/locale/lt-LT.ini index 8e59372..97e889f 100644 --- a/UI/data/locale/lt-LT.ini +++ b/UI/data/locale/lt-LT.ini @@ -326,3 +326,6 @@ Push-to-talk="Kalbėti paspaudus" FinalScene.Title="Ištrinti sceną" FinalScene.Text="Turi būti bent viena scena." + + + diff --git a/UI/data/locale/ms-MY.ini b/UI/data/locale/ms-MY.ini index c0b7375..1413cc0 100644 --- a/UI/data/locale/ms-MY.ini +++ b/UI/data/locale/ms-MY.ini @@ -52,11 +52,34 @@ Reset="Set semula" Hours="Jam" Minutes="Minit" Seconds="Saat" +Import="Import" +Export="Eksport" +Copy="Salin" +Paste="Tampal" +PasteReference="Tampal (rujukan)" +PasteDuplicate="Tampal (dua salinan)" +Next="Seterusnya" +Back="Kembali" +BandwidthTest.Region.EU="Eropah" +BandwidthTest.Region.Asia="Asia" +BandwidthTest.Region.Other="Lain-lain" +Basic.AutoConfig.ApplySettings="Gunakan seting" +Basic.AutoConfig.StartPage="Maklumat penggunaan" +Basic.AutoConfig.VideoPage="Seting video" +Basic.AutoConfig.VideoPage.BaseResolution.UseCurrent="Gunakan semasa (%1x%2)" +Basic.AutoConfig.VideoPage.BaseResolution.Display="Paparan %1 (%2x%3)" +Basic.AutoConfig.VideoPage.FPS.UseCurrent="Gunakan semasa (%1)" +Basic.AutoConfig.StreamPage="Maklumat aliran" +Basic.AutoConfig.StreamPage.SubTitle="Sila taip maklumat aliran anda" +Basic.AutoConfig.StreamPage.Service="Perkhidmatan-perkhidmatan" +Basic.AutoConfig.StreamPage.Service.ShowAll="Tunjuk semua..." +Basic.AutoConfig.StreamPage.Server="Server" +Basic.AutoConfig.StreamPage.StreamKey.LinkToSite="(Link)" @@ -428,3 +451,6 @@ Push-to-talk="Tekan-untuk-cakap" + + + diff --git a/UI/data/locale/nb-NO.ini b/UI/data/locale/nb-NO.ini index 82be312..35561a4 100644 --- a/UI/data/locale/nb-NO.ini +++ b/UI/data/locale/nb-NO.ini @@ -78,6 +78,8 @@ None="Ingen" StudioMode.Preview="Forhåndsvisning" StudioMode.Program="Program" ShowInMultiview="Vis i Multiview" +VerticalLayout="Loddrett oppsett" +Group="Gruppe" AlreadyRunning.Title="OBS kjører allerede" AlreadyRunning.Text="OBS kjører allerede! Med mindre du ikke mente dette, vennligst lukk alle eksisterende kjørende tilfeller av OBS før du kjører noen nye. Hvis du har satt OBS til å minimere til systemkurven, vennligst sjekk om den fortsatt kjører der." @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="Stopper omspillingsbufferen..." Basic.Main.StopStreaming="Stopp Strømming" Basic.Main.StoppingStreaming="Stanser strøm…" Basic.Main.ForceStopStreaming="Stopp strømming (forkast forsinkelse)" +Basic.Main.Group="Gruppe %1" +Basic.Main.GroupItems="Gruppér merkede gjenstander" +Basic.Main.Ungroup="Adskill" Basic.MainMenu.File="&Fil" Basic.MainMenu.File.Export="&Eksportér" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="&Verktøy" Basic.MainMenu.Help="&Hjelp" Basic.MainMenu.Help.HelpPortal="&Portal for hjelp" Basic.MainMenu.Help.Website="Besøk &nettstedet" +Basic.MainMenu.Help.Discord="Bli med i &Discord-serveren" Basic.MainMenu.Help.Logs="&Loggfiler" Basic.MainMenu.Help.Logs.ShowLogs="Vis &loggfiler" Basic.MainMenu.Help.Logs.UploadCurrentLog="Last opp &nåværende loggfil" Basic.MainMenu.Help.Logs.UploadLastLog="Last opp &siste loggfil" Basic.MainMenu.Help.Logs.ViewCurrentLog="&Vis gjeldende logg" Basic.MainMenu.Help.CheckForUpdates="Se etter oppdateringer" +Basic.MainMenu.Help.CrashLogs="Krasjrapporter" +Basic.MainMenu.Help.CrashLogs.ShowLogs="Vi&s krasjrapporter" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="Last opp den &nyeste krasjrapporten" Basic.Settings.ProgramRestart="Programmet må startes på nytt for at disse innstillingene skal tre i kraft." Basic.Settings.ConfirmTitle="Bekreft endringer" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Alltid minimere til systemstatusf Basic.Settings.General.SaveProjectors="Lagre projektorer ved avslutning" Basic.Settings.General.SwitchOnDoubleClick="Dobbeltklikking vil gå til scenen" Basic.Settings.General.StudioPortraitLayout="Aktiver portrett/loddrett vindu" +Basic.Settings.General.Multiview="Flervisning" +Basic.Settings.General.Multiview.MouseSwitch="Klikk for å bytte mellom scener" +Basic.Settings.General.Multiview.DrawSourceNames="Vis scenenes navn" +Basic.Settings.General.Multiview.DrawSafeAreas="Tegn trygge områder (EBU R 95)" Basic.Settings.General.MultiviewLayout="Flervisningsplassering" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Vannrett, øverst" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Vannrett, nederst" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Loddrett, venstre" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Loddrett, høyre" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Vannrett, topp (8 scener)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Vannrett, bunn (8 scener)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Loddrett, venstre (8 scener)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Loddrett, høyre (8 scener)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Vannrett, topp (24 scener)" Basic.Settings.Stream="Strøm" Basic.Settings.Stream.StreamType="Strømmetype" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="Audiometerets forfallsfrekvens" Basic.Settings.Audio.MeterDecayRate.Fast="Raskt" Basic.Settings.Audio.MeterDecayRate.Medium="Middels (Type I PPM)" Basic.Settings.Audio.MeterDecayRate.Slow="Tregt (Type II PPM)" +Basic.Settings.Audio.PeakMeterType="Toppunktmålertype" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Samplingstoppunkt" +Basic.Settings.Audio.PeakMeterType.TruePeak="Ekte toppunkt (Høyere CPU-bruk)" Basic.Settings.Audio.MultiChannelWarning.Enabled="Advarsel: Surroundlyd er aktivert." Basic.Settings.Audio.MultichannelWarning="Hvis du strømmer, sjekk om strømmetjenesten din støtter både surround-lydinnføring og surround-lydavspilling. Twitch, Facebook 360 Live, Mixer RTMP, og Smashcast er eksempler hvor surroundlyd er full støttet. Selv om Facebook Live og YouTube Live begge støtter surround-innføring, nedmikser Facebook Live det ned til Stereo, og YouTube Live spiller bare av to kanaler.\n\nOBS-lydfiltre er kompatible med surroundlyd, selv om VST-tilleggsstøtte ikke er garantert." Basic.Settings.Audio.MultichannelWarning.Title="Vil du aktivere surround-lyd?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="Nettverk" Basic.Settings.Advanced.Network.BindToIP="Bind til IP" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Aktiver den nye nettverkskoden" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Lavlatens-modus" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Deaktiver hurtigtaster når hovedvinduet er i fokus" Basic.AdvAudio="Avanserte lydinnstillinger" Basic.AdvAudio.Name="Navn" @@ -749,3 +767,11 @@ OutputWarnings.MP4Recording="Advarsel: Opptak lagret i MP4 bil bli slettet hvis FinalScene.Title="Slett scene" FinalScene.Text="Det må være minst én scene." +NoSources.Title="Ingen kilder" +NoSources.Text="Det ser ut som du ikke har lagt til noen videokilder ennå, så du vil kun sende en blank skjerm. Er du sikker på at du vil gjøre dette?" +NoSources.Text.AddSource="Du kan legge til kilder ved å klikke på +-ikonet under Kilder-boksen i hovedvinduet til enhver tid." + +ChangeBG="Velg farge" +CustomColor="Egendefinert farge" + + diff --git a/UI/data/locale/nl-NL.ini b/UI/data/locale/nl-NL.ini index 07ede93..34ceac9 100644 --- a/UI/data/locale/nl-NL.ini +++ b/UI/data/locale/nl-NL.ini @@ -42,7 +42,7 @@ Clear="Wissen" Revert="Herstellen" Show="Weergeven" Hide="Verbergen" -UnhideAll="Verberge alle" +UnhideAll="Allemaal zichtbaar maken" Untitled="Naamloos" New="Nieuw" Duplicate="Dupliceren" @@ -78,6 +78,8 @@ None="Geen" StudioMode.Preview="Preview" StudioMode.Program="Programma" ShowInMultiview="Weergeven in Multiview" +VerticalLayout="Verticale Lay-out" +Group="Groep" AlreadyRunning.Title="OBS is al actief" AlreadyRunning.Text="OBS is al actief! Tenzij je dit wilde doen, sluit a.u.b. alle reeds draaiende instanties van OBS voor je een nieuwe instantie opstart. Als je OBS hebt ingesteld om naar het systeemvak te minimaliseren, controleer dan of hij daar nog staat." @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="Replay Buffer aan het stoppen..." Basic.Main.StopStreaming="Stream Stoppen" Basic.Main.StoppingStreaming="Stream Stoppen..." Basic.Main.ForceStopStreaming="Stop Stream (vertraging negeren)" +Basic.Main.Group="Groep %1" +Basic.Main.GroupItems="Groepeer geselecteerde items" +Basic.Main.Ungroup="Degroeperen" Basic.MainMenu.File="&Bestand" Basic.MainMenu.File.Export="&Exporteren" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="&Tools" Basic.MainMenu.Help="&Help" Basic.MainMenu.Help.HelpPortal="Help-&portal" Basic.MainMenu.Help.Website="&Website Bezoeken" +Basic.MainMenu.Help.Discord="Wordt lid van &Discord server" Basic.MainMenu.Help.Logs="&Logbestanden" Basic.MainMenu.Help.Logs.ShowLogs="Logbe&standen Weergeven" Basic.MainMenu.Help.Logs.UploadCurrentLog="Upload &Huidige Logbestand" Basic.MainMenu.Help.Logs.UploadLastLog="Upload &Laatste Logbestand" Basic.MainMenu.Help.Logs.ViewCurrentLog="Toon Huidige Logbestand (&V)" Basic.MainMenu.Help.CheckForUpdates="Controleer Op Updates" +Basic.MainMenu.Help.CrashLogs="Fouten&rapport" +Basic.MainMenu.Help.CrashLogs.ShowLogs="Toon crashrapporten (&S)" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="Upload &laatste crash rapport" Basic.Settings.ProgramRestart="Het programma moet opnieuw worden opgestart om deze instellingen te activeren." Basic.Settings.ConfirmTitle="Wijzigingen Bevestigen" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Altijd minimaliseren naar het sys Basic.Settings.General.SaveProjectors="Projectors opslaan bij afsluiten" Basic.Settings.General.SwitchOnDoubleClick="Ga over naar scène bij dubbelklik" Basic.Settings.General.StudioPortraitLayout="Portret/verticale layout inschakelen" +Basic.Settings.General.Multiview="Multiview" +Basic.Settings.General.Multiview.MouseSwitch="Klik hier om tussen scènes te wisselen" +Basic.Settings.General.Multiview.DrawSourceNames="Toon namen van scènes" +Basic.Settings.General.Multiview.DrawSafeAreas="Veilige gebieden (EBU R 95) tekenen" Basic.Settings.General.MultiviewLayout="Multiview-indeling" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horizontaal, Boven" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horizontaal, Onder" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Verticaal, Links" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Verticaal, Rechts" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horizontaal, boven (8 scènes)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horizontaal, onder (8 scènes)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Verticaal, links (8 scènes)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Verticaal, rechts (8 scènes)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Horizontaal, boven (24 scènes)" Basic.Settings.Stream="Stream" Basic.Settings.Stream.StreamType="Stream Type" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="Vervalsnelheid Volumemeter" Basic.Settings.Audio.MeterDecayRate.Fast="Snel" Basic.Settings.Audio.MeterDecayRate.Medium="Gemiddeld (Type I PPM)" Basic.Settings.Audio.MeterDecayRate.Slow="Traag (Type II PPM)" +Basic.Settings.Audio.PeakMeterType="Piek Meter Type" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Sample piek" +Basic.Settings.Audio.PeakMeterType.TruePeak="True piek (hogere CPU-gebruik)" Basic.Settings.Audio.MultiChannelWarning.Enabled="Waarschuwing: Surround sound audio is ingeschakeld." Basic.Settings.Audio.MultichannelWarning="Als je streamt, controleer dan of je streaming service zowel surround sound ingest als surround sound afspelen ondersteunt. Twitch, Facebook 360 Live, Mixer RTMP, Smashcast zijn voorbeelden waar surround sound volledig ondersteund is. Alhoewel Facebook Live en Youtube Live beide surround ingest ondersteunen, downmixt Facebook Live het naar stereo, terwijl Youtube Live slechts twee kanalen afspeelt.\n\nOBS audio filters kunnen overweg met surround sound, maar ondersteuning bij VST plugins is niet gegarandeerd." Basic.Settings.Audio.MultichannelWarning.Title="Surround sound audio inschakelen?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="Netwerk" Basic.Settings.Advanced.Network.BindToIP="Bind aan IP" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Schakel nieuwe netwerkcode in" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Lage latency modus" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Hotkeys uitschakelen wanneer hoofdvenster in focus is" Basic.AdvAudio="Geavanceerde Audioinstellingen" Basic.AdvAudio.Name="Naam" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="Waarschuwing: Opnames opgeslagen als MP4 zijn niet FinalScene.Title="Verwijder scène" FinalScene.Text="Er moet tenminste één scène zijn." +NoSources.Title="Geen bronnen" +NoSources.Text="Er zijn nog geen video bronnen toegevoegd, er zal dus een leeg scherm weergegeven worden. Weet je zeker dat je dit wilt doen?" +NoSources.Text.AddSource="Bronnen kunnen toegevoegd worden door op het + icoon onder het vak \"Bronnen\" in het hoofdvenster te klikken." + +ChangeBG="Kleur instellen" +CustomColor="Aangepaste kleur" + +BrowserSource.EnableHardwareAcceleration="Browser bron hardwareversnelling inschakelen" + diff --git a/UI/data/locale/nn-NO.ini b/UI/data/locale/nn-NO.ini index 9ddedc3..582e19a 100644 --- a/UI/data/locale/nn-NO.ini +++ b/UI/data/locale/nn-NO.ini @@ -197,6 +197,9 @@ Basic.Settings.General.Language="Språk" + + + diff --git a/UI/data/locale/pl-PL.ini b/UI/data/locale/pl-PL.ini index ace22ef..3ed7ca6 100644 --- a/UI/data/locale/pl-PL.ini +++ b/UI/data/locale/pl-PL.ini @@ -78,6 +78,8 @@ None="Brak" StudioMode.Preview="Podgląd" StudioMode.Program="Program" ShowInMultiview="Pokaż w Multiview" +VerticalLayout="Układ pionowy" +Group="Grupa" AlreadyRunning.Title="OBS jest już uruchomiony" AlreadyRunning.Text="OBS jest już uruchomiony! Sprawdź wszystkie uruchomione wystąpienia OBS zanim uruchomisz go jeszcze raz. Jeżeli OBS jest zminimalizowany do zasobnika systemowego, sprawdź czy nie jest uruchomiony także w tym miejscu." @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="Zatrzymywanie buforu replay..." Basic.Main.StopStreaming="Zatrzymaj stream" Basic.Main.StoppingStreaming="Zatrzymywanie streamowania..." Basic.Main.ForceStopStreaming="Zatrzymaj stream (anuluj opóźnienie)" +Basic.Main.Group="Grupa %1" +Basic.Main.GroupItems="Grupuj wybrane elementy" +Basic.Main.Ungroup="Rozgrupuj" Basic.MainMenu.File="&Plik" Basic.MainMenu.File.Export="&Eksport" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="&Narzędzia" Basic.MainMenu.Help="P&omoc" Basic.MainMenu.Help.HelpPortal="&Portal pomocy" Basic.MainMenu.Help.Website="Od&wiedź naszą stronę" +Basic.MainMenu.Help.Discord="Dołącz do serwera &Discord" Basic.MainMenu.Help.Logs="P&liki dziennika" Basic.MainMenu.Help.Logs.ShowLogs="Pokaż pliki dziennika (&s)" Basic.MainMenu.Help.Logs.UploadCurrentLog="Wyślij &aktualny plik dziennika" Basic.MainMenu.Help.Logs.UploadLastLog="Wyślij o&statni plik dziennika" Basic.MainMenu.Help.Logs.ViewCurrentLog="Podgląd akty&wnego pliku dziennika" Basic.MainMenu.Help.CheckForUpdates="Sprawdź dostępność aktualizacji" +Basic.MainMenu.Help.CrashLogs="&Raporty o awariach" +Basic.MainMenu.Help.CrashLogs.ShowLogs="Pokaż raporty o awariach (&S)" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="Wyślij ostatni raport o awariach (&L)" Basic.Settings.ProgramRestart="Aby te ustawienia zaczęły obowiązywać, należy ponownie uruchomić program." Basic.Settings.ConfirmTitle="Potwierdź zmiany" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Zawsze minimalizuj do zasobnika s Basic.Settings.General.SaveProjectors="Zapisz konfigurację podglądów na pełnym ekranie przy zamknięciu aplikacji" Basic.Settings.General.SwitchOnDoubleClick="Przejdź do sceny po dwukrotnym kliknięciu" Basic.Settings.General.StudioPortraitLayout="Włącz układ pionowy" +Basic.Settings.General.Multiview="Multiview" +Basic.Settings.General.Multiview.MouseSwitch="Kliknij, aby przełączać się między scenami" +Basic.Settings.General.Multiview.DrawSourceNames="Pokaż nazwy scen" +Basic.Settings.General.Multiview.DrawSafeAreas="Rysowanie bezpiecznych obszarów (EBU R 95)" Basic.Settings.General.MultiviewLayout="Podgląd wielokrotny" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Poziomo, Góra" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Poziomo, Dół" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Pionowo, Lewo" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Pionowo, Prawo" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Poziomo, Góra (8 scen)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Poziomo, Dół (8 scen)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Pionowo, Lewo (8 scen)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Pionowo, Prawo (8 scen)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Poziomo, Góra (24 sceny)" Basic.Settings.Stream="Stream" Basic.Settings.Stream.StreamType="Typ streamu" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="Szybkość powrotu miernika audio" Basic.Settings.Audio.MeterDecayRate.Fast="Szybko" Basic.Settings.Audio.MeterDecayRate.Medium="Średnio (PPM typu I)" Basic.Settings.Audio.MeterDecayRate.Slow="Wolno (PPM typu II)" +Basic.Settings.Audio.PeakMeterType="Typ szczytowego poziomu paska audio" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Poziom szczytowy próbkowany" +Basic.Settings.Audio.PeakMeterType.TruePeak="Rzeczywisty szczytowy poziom (większe użycie procesora)" Basic.Settings.Audio.MultiChannelWarning.Enabled="Uwaga: Dźwięk przestrzenny jest włączony." Basic.Settings.Audio.MultichannelWarning="W przypadku streamowania sprawdź, czy docelowa usługa obsługuje wielokanałowe wejście oraz odtwarzanie dźwięku surround. Twitch, Facebook, 360 Live, Mixer RTMP, Smashcast to przykłady usług obsługujących dźwięk wielokanałowy. O ile Facebook Live i Youtube Live obsługują wejście dźwięku wielokanałowego, to w przypadku odtwarzania Facebook miksuje kanału do stereo a Youtube Live odtwarza tylko dwa kanały.\n\nFiltry dźwiękowe OBS są w pełni kompatybilne z dźwiękiem kanałowym, natomiast wsparcie pluginów VST nie jest gwarantowane." Basic.Settings.Audio.MultichannelWarning.Title="Włączyć dźwięk przestrzenny?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="Sieć" Basic.Settings.Advanced.Network.BindToIP="Przypisane IP" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Aktywuj nowy kod sieciowy" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Tryb niskich opóźnień" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Wyłącz skróty klawiszowe, gdy główne okno programu jest aktywne na pierwszym planie" Basic.AdvAudio="Zaawansowane ustawienia dźwięku" Basic.AdvAudio.Name="Nazwa" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="Ostrzeżenie: Nagrania zapisanego w formacie mp4 ni FinalScene.Title="Usuń scenę" FinalScene.Text="Musi być co najmniej jedna scena." +NoSources.Title="Brak źródeł" +NoSources.Text="Wygląda na to, że nie dodano żadnych źródeł video. Na wyjściu otrzymasz tylko czarny ekran. Czy na pewno chcesz to zrobić?" +NoSources.Text.AddSource="Źródła dodać można w każdym momencie klikając przycisk + w okienku Źródła głównego ekranu aplikacji." + +ChangeBG="Ustaw kolor" +CustomColor="Kolor niestandardowy" + +BrowserSource.EnableHardwareAcceleration="Włącz akcelerację sprzętową w pluginie przeglądarki" + diff --git a/UI/data/locale/pt-BR.ini b/UI/data/locale/pt-BR.ini index a036101..5938697 100644 --- a/UI/data/locale/pt-BR.ini +++ b/UI/data/locale/pt-BR.ini @@ -78,6 +78,8 @@ None="Nenhuma" StudioMode.Preview="Prévia" StudioMode.Program="Programa" ShowInMultiview="Mostrar no Multiview" +VerticalLayout="Layout Vertical" +Group="Grupo" AlreadyRunning.Title="OBS já está em execução" AlreadyRunning.Text="OBS já está em execução! A menos que você tenha a intenção de fazer isso, por favor, feche todas as instâncias existentes do OBS antes de tentar executar uma nova. Se você tiver definido para minimizar o OBS na bandeja do sistema, verifique se ainda está lá em execução." @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="Parando Buffer do Replay..." Basic.Main.StopStreaming="Parar Transmissão" Basic.Main.StoppingStreaming="Parando Transmissão..." Basic.Main.ForceStopStreaming="Pare de transmitir (descartar atraso)" +Basic.Main.Group="Grupo %1" +Basic.Main.GroupItems="Agrupar Itens Selecionados" +Basic.Main.Ungroup="Desagrupar" Basic.MainMenu.File="&Arquivo" Basic.MainMenu.File.Export="&Exportar" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="Ferramentas (&T)" Basic.MainMenu.Help="&Ajuda" Basic.MainMenu.Help.HelpPortal="&Portal de Ajuda" Basic.MainMenu.Help.Website="Visitar &website" +Basic.MainMenu.Help.Discord="Juntar-se ao Servidor do &Discord" Basic.MainMenu.Help.Logs="&Arquivos de Log" Basic.MainMenu.Help.Logs.ShowLogs="&Mostrar Arquivos de Log" Basic.MainMenu.Help.Logs.UploadCurrentLog="Enviar &Arquivo de Log Atual" Basic.MainMenu.Help.Logs.UploadLastLog="Enviar &Ultimo Arquivo de Log" Basic.MainMenu.Help.Logs.ViewCurrentLog="&Exibir Log atual" Basic.MainMenu.Help.CheckForUpdates="Verificar se há atualizações" +Basic.MainMenu.Help.CrashLogs="&Relatórios de erros" +Basic.MainMenu.Help.CrashLogs.ShowLogs="Exibir relatórios de erro&s" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="Enviar ú<imo relatório de erros" Basic.Settings.ProgramRestart="O Programa precisar ser reiniciado para que estas configurações surtam efeito." Basic.Settings.ConfirmTitle="Confirmar Alterações" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Sempre minimizar para a bandeja ( Basic.Settings.General.SaveProjectors="Salvar projetores ao sair" Basic.Settings.General.SwitchOnDoubleClick="Mudar para a cena quando clicar duas vezes" Basic.Settings.General.StudioPortraitLayout="Ativar o layout Paisagem/Retrato" +Basic.Settings.General.Multiview="Multiview" +Basic.Settings.General.Multiview.MouseSwitch="Clique para alternar entre cenas" +Basic.Settings.General.Multiview.DrawSourceNames="Mostrar nome das cenas" +Basic.Settings.General.Multiview.DrawSafeAreas="Mostrar áreas de segurança (EBU R 95)" Basic.Settings.General.MultiviewLayout="Layout do Multiview" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horizontal, Acima" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horizontal, Abaixo" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertical, à Esquerda" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertical, à Direita" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horizontal, Acima (8 Cenas)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horizontal, Abaixo (8 Cenas)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertical, à Esquerda (8 Cenas)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertical, à Direita (8 Cenas)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Horizontal, Acima (24 Cenas)" Basic.Settings.Stream="Stream" Basic.Settings.Stream.StreamType="Tipo de Stream" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="Taxa de Decaimento do Medidor de Áudio" Basic.Settings.Audio.MeterDecayRate.Fast="Rápida" Basic.Settings.Audio.MeterDecayRate.Medium="Média (PPM Tipo I)" Basic.Settings.Audio.MeterDecayRate.Slow="Devagar (PPM Tipo II)" +Basic.Settings.Audio.PeakMeterType="Tipo do medidor de pico" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Exemplo de pico" +Basic.Settings.Audio.PeakMeterType.TruePeak="True Peak (Uso elevado de CPU)" Basic.Settings.Audio.MultiChannelWarning.Enabled="AVISO: Áudio Surround está habilitado." Basic.Settings.Audio.MultichannelWarning="Antes de transmitir, verifique se o seu serviço de transmissão suporta tanto receber como reproduzir som surround. Twitch, Facebook 360 Live, Mixer RTMP, Smashcast são exemplos onde o som surround é totalmente suportado. Embora o Facebook Live e o YouTube Live ambos aceitem receber som surround, o Facebook Live transformará para estéreo e o YouTube Live reproduz apenas dois canais.\n\nOs filtros de áudio do OBS são compatíveis com o som surround, embora o suporte do plugin VST não seja garantido." Basic.Settings.Audio.MultichannelWarning.Title="Ativar o som surround?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="Rede" Basic.Settings.Advanced.Network.BindToIP="Transmitir pelo IP" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Habilitar o novo código de rede" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Modo de baixa latência" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Desativar teclas de atalho quando a janela principal estiver em foco" Basic.AdvAudio="Propriedades de áudio avançadas" Basic.AdvAudio.Name="Nome" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="Atenção: Gravações salvas em arquivos MP4 se to FinalScene.Title="Excluir cena" FinalScene.Text="É preciso haver pelo menos uma cena." +NoSources.Title="Sem Fontes" +NoSources.Text="Parece que você ainda não adicionou nenhuma fonte de vídeo, portanto, você só exibirá uma tela em branco. Você tem certeza de que quer fazer isso?" +NoSources.Text.AddSource="Você pode adicionar fontes clicando no ícone + sob a caixa de Fontes na janela principal, a qualquer momento." + +ChangeBG="Definir Cor" +CustomColor="Cor Personalizada" + +BrowserSource.EnableHardwareAcceleration="Habilitar a aceleração por Hardware do Navegador" + diff --git a/UI/data/locale/pt-PT.ini b/UI/data/locale/pt-PT.ini index 25c82a7..5c6cd0e 100644 --- a/UI/data/locale/pt-PT.ini +++ b/UI/data/locale/pt-PT.ini @@ -478,10 +478,6 @@ Basic.Settings.General.RecordWhenStreaming="Gravar automaticamente quando estive Basic.Settings.General.KeepRecordingWhenStreamStops="Continuar a gravar quando a transmissão parar" Basic.Settings.General.SysTray="Bandeja do Sistema" Basic.Settings.General.SysTrayWhenStarted="Minimizar para a área de notificações quando iniciado" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horizontal, Topo" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horizontal, Inferior" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertical, Esquerda" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertical, Direita" Basic.Settings.Stream="Transmissão" Basic.Settings.Stream.StreamType="Tipo de transmissão" @@ -697,3 +693,6 @@ OutputWarnings.MultiTrackRecording="Aviso: Alguns formatos (como FLV) não supor FinalScene.Title="Apagar Cena" + + + diff --git a/UI/data/locale/ro-RO.ini b/UI/data/locale/ro-RO.ini index 0c3b094..43208f0 100644 --- a/UI/data/locale/ro-RO.ini +++ b/UI/data/locale/ro-RO.ini @@ -598,3 +598,6 @@ OutputWarnings.MultiTrackRecording="Atenție: Anumite formate (precum FLV) nu su FinalScene.Text="Trebuie să existe cel puțin o scenă." + + + diff --git a/UI/data/locale/ru-RU.ini b/UI/data/locale/ru-RU.ini index 39a90ce..cb2a1a7 100644 --- a/UI/data/locale/ru-RU.ini +++ b/UI/data/locale/ru-RU.ini @@ -78,6 +78,8 @@ None="Нет" StudioMode.Preview="Предпросмотр" StudioMode.Program="Программа" ShowInMultiview="Отображать в Мульти-обзоре" +VerticalLayout="Вертикальное расположение" +Group="Группа" AlreadyRunning.Title="OBS уже запущен" AlreadyRunning.Text="OBS уже запущен! Пожалуйста, закройте все запущенные экземпляры OBS перед попыткой запустить новые (только если вы не хотели именно этого). Если вы настроили OBS на сворачивание в системный трей, пожалуйста, проверьте, возможно он до сих пор запущен." @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="Остановка повтора..." Basic.Main.StopStreaming="Остановить трансляцию" Basic.Main.StoppingStreaming="Остановка вещания..." Basic.Main.ForceStopStreaming="Остановить трансляцию (сбросить задержку)" +Basic.Main.Group="Группа %1" +Basic.Main.GroupItems="Сгруппировать выбранные элементы" +Basic.Main.Ungroup="Разгруппировать" Basic.MainMenu.File="&Файл" Basic.MainMenu.File.Export="&Экспорт" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="&Инструменты" Basic.MainMenu.Help="&Справка" Basic.MainMenu.Help.HelpPortal="&Портал помощи" Basic.MainMenu.Help.Website="Посетить &веб-сайт" +Basic.MainMenu.Help.Discord="Зайти на сервер &Discord" Basic.MainMenu.Help.Logs="&Log файлы" Basic.MainMenu.Help.Logs.ShowLogs="&Показать лог-файлы" Basic.MainMenu.Help.Logs.UploadCurrentLog="Загрузить &текущий Log файл" Basic.MainMenu.Help.Logs.UploadLastLog="Загрузить &последний Log файл" Basic.MainMenu.Help.Logs.ViewCurrentLog="&Просмотреть текущий журнал" Basic.MainMenu.Help.CheckForUpdates="Проверить наличие обновлений" +Basic.MainMenu.Help.CrashLogs="&Отчёты об ошибках" +Basic.MainMenu.Help.CrashLogs.ShowLogs="&Показать отчёты об ошибках" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="&Загрузить последний отчёт об ошибке" Basic.Settings.ProgramRestart="Для изменения этих параметров требуется перезапустить программу." Basic.Settings.ConfirmTitle="Подтверждить Изменения" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Всегда сворачиват Basic.Settings.General.SaveProjectors="Сохранять проекторы при выходе" Basic.Settings.General.SwitchOnDoubleClick="Переход к сцене при двойном щелчке" Basic.Settings.General.StudioPortraitLayout="Включить портретное/вертикальное расположение" +Basic.Settings.General.Multiview="Мульти-обзор" +Basic.Settings.General.Multiview.MouseSwitch="Переключение сцен щелчком мыши" +Basic.Settings.General.Multiview.DrawSourceNames="Показывать названия сцен" +Basic.Settings.General.Multiview.DrawSafeAreas="Показывать зоны безопасности (EBU R 95)" Basic.Settings.General.MultiviewLayout="Размещение мульти-обзора" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Горизонтально, вверху" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Горизонтально, внизу" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Вертикально, слева" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Вертикально, справа" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Горизонтально, верх (8 сцен)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Горизонтально, низ (8 сцен)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Вертикально, слева (8 сцен)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Вертикально, справа (8 сцен)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Горизонтально, верх (24 сцены)" Basic.Settings.Stream="Вещание" Basic.Settings.Stream.StreamType="Тип вещания" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="Скорость спада аудиоме Basic.Settings.Audio.MeterDecayRate.Fast="Быстро" Basic.Settings.Audio.MeterDecayRate.Medium="Средне (PPM типа I)" Basic.Settings.Audio.MeterDecayRate.Slow="Медленно (PPM типа II)" +Basic.Settings.Audio.PeakMeterType="Тип измерителя пиков" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Упрощенный" +Basic.Settings.Audio.PeakMeterType.TruePeak="Точный (Повышенное использование ЦП)" Basic.Settings.Audio.MultiChannelWarning.Enabled="ВНИМАНИЕ: Объемный звук включен." Basic.Settings.Audio.MultichannelWarning="При запуске стрима проверьте, поддерживает ли ваш сервис потокового вещания объемный звук (как запись, так и воспроизведение). Twitch, Facebook 360 Live, Mixer RTMP, Smashcast — примеры сервисов, полностью поддерживающих объемное звучание. Несмотря на то, что Facebook Live и Youtube Live принимают объёмный звук, Facebook Live микширует его в стерео, а Youtube Live воспроизводит только два канала.\n\nЗвуковые фильтры OBS совместимы с объемным звучанием, хотя поддержка плагинов VST не гарантируется." Basic.Settings.Audio.MultichannelWarning.Title="Включить объемный звук?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="Сеть" Basic.Settings.Advanced.Network.BindToIP="Привязать к IP" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Включить новый сетевой код" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Режим низкой задержки" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Отключить горячие клавиши, если главное окно находится в фокусе" Basic.AdvAudio="Расширенные свойства аудио" Basic.AdvAudio.Name="Название" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="Внимание: Записи, сохраненн FinalScene.Title="Удалить сцену" FinalScene.Text="Здесь должна быть по крайней мере одна сцена." +NoSources.Title="Нет источников" +NoSources.Text="Похоже, вы еще не добавили ни одного источника. Вы будете выводить только пустой экран. Вы уверены, что хотите этого?" +NoSources.Text.AddSource="Вы можете добавить источники, нажав иконку + под окном Источники в главном окне в любое время." + +ChangeBG="Установить цвет" +CustomColor="Пользовательский цвет" + +BrowserSource.EnableHardwareAcceleration="Включить аппаратное ускорение Браузера" + diff --git a/UI/data/locale/sk-SK.ini b/UI/data/locale/sk-SK.ini index fc9bb99..fd45ee8 100644 --- a/UI/data/locale/sk-SK.ini +++ b/UI/data/locale/sk-SK.ini @@ -78,6 +78,8 @@ None="Nič" StudioMode.Preview="Náhľad" StudioMode.Program="Program" ShowInMultiview="Zobraziť v Multiview" +VerticalLayout="Vertikálne rozloženie" +Group="Skupina" AlreadyRunning.Title="OBS je už spustený" AlreadyRunning.Text="OBS je už spustená! Prosím vypnite všetky existujúce inštancie OBS pred pokusom o spustenie novej inštancie. Ak máte OBS minimalizovaný do systémovej lišty, prosím skontrolujte či tam stále beží." @@ -112,28 +114,41 @@ Basic.AutoConfig.VideoPage.FPS.PreferHighFPS="60 alebo 30, ale radšej 60, ak je Basic.AutoConfig.VideoPage.FPS.PreferHighRes="60 alebo 30, ale radšej vysoké rozlíšenie" Basic.AutoConfig.VideoPage.CanvasExplanation="Poznámka: rozlíšenie (základné) plátna nie je nevyhnutne rovnaké ako rozlíšenie v ktorom budete streamovať alebo nahrávať. Skutočné streamovacie/nahrávacie rozlíšenie môže byť zmenšené preto aby sa znížilo používanie dát alebo zdrojov." Basic.AutoConfig.StreamPage="Informácie o streame" +Basic.AutoConfig.StreamPage.SubTitle="Prosím, zadajte svoje údaje o vysielaní" Basic.AutoConfig.StreamPage.Service="Služba" Basic.AutoConfig.StreamPage.Service.ShowAll="Zobraziť všetky..." Basic.AutoConfig.StreamPage.Server="Server" Basic.AutoConfig.StreamPage.StreamKey="Streamovací kľúč" Basic.AutoConfig.StreamPage.StreamKey.LinkToSite="(Odkaz)" +Basic.AutoConfig.StreamPage.PerformBandwidthTest="Odhadnúť bitrate pomocou testu rýchlosti pripojenia (môže to trvať pár minút)" Basic.AutoConfig.StreamPage.PreferHardwareEncoding="Preferovať hardvérové enkódovanie" +Basic.AutoConfig.StreamPage.PreferHardwareEncoding.ToolTip="Hardwarové enkódovanie eliminuje väčšinu využitia CPU, ale na dosiahnutie rovnakej kvality videa môže vyžadovať väčší bitrate." Basic.AutoConfig.StreamPage.StreamWarning.Title="Upozornenie o streame" +Basic.AutoConfig.StreamPage.StreamWarning.Text="Test rýchlosti pripojenia vysiela obraz bez zvuku s náhodnými dátami. Odporúčame dočasne zakázať záznam vysielaní a nastaviť živý prenos ako súkromný, kým sa celý test nedokončí. Pokračovať?" Basic.AutoConfig.TestPage="Konečné výsledky" +Basic.AutoConfig.TestPage.SubTitle.Testing="Program práve vykonáva sériu testov pre odhad optimálneho nastavenia" Basic.AutoConfig.TestPage.SubTitle.Complete="Testovanie dokončené" +Basic.AutoConfig.TestPage.TestingBandwidth="Prebieha testovanie rýchlosti pripojenia. Toto môže chvíľku trvať..." Basic.AutoConfig.TestPage.TestingBandwidth.Connecting="Pripájanie k: %1..." +Basic.AutoConfig.TestPage.TestingBandwidth.ConnectFailed="Nepodarilo sa pripojiť k žiadnemu serveru. Skontrolujte vaše internetové pripojenie a skúste to znovu." +Basic.AutoConfig.TestPage.TestingBandwidth.Server="Testovanie rýchlosti pripojenia pre: %1" Basic.AutoConfig.TestPage.TestingStreamEncoder="Testovanie streamovacieho enkodéra, toto môže trvať minútu..." Basic.AutoConfig.TestPage.TestingRecordingEncoder="Testovanie nahrávacieho enkodéra, toto môže trvať minútu..." Basic.AutoConfig.TestPage.TestingRes="Testovanie rozlíšení, toto môže trvať niekoľko minút..." Basic.AutoConfig.TestPage.TestingRes.Fail="Nepodarilo sa spustiť enkodér" Basic.AutoConfig.TestPage.TestingRes.Resolution="Testovanie %1x%2 %3 FPS..." -Basic.AutoConfig.TestPage.Result.StreamingEncoder="Streamovací Enkodér" -Basic.AutoConfig.TestPage.Result.RecordingEncoder="Nahrávací Enkodér" +Basic.AutoConfig.TestPage.Result.StreamingEncoder="Kodér vysielania" +Basic.AutoConfig.TestPage.Result.RecordingEncoder="Kodér nahrávania" +Basic.AutoConfig.TestPage.Result.Header="Program zistil, že nasledujúce nastavenia by mohli byť pre vás najvhodnejšie:" +Basic.AutoConfig.TestPage.Result.Footer="Pre použitie týchto nastavení kliknite na Použiť. Pre úpravu nastavení sprievodcu kliknite na Späť. Ak si želáte nastaviť program manuálne, kliknite na Zrušiť a následne na tlačidlo Nastavenia." Basic.Stats="Štatistiky" Basic.Stats.CPUUsage="Využitie CPU" Basic.Stats.HDDSpaceAvailable="Dostupné miesto na HDD" Basic.Stats.MemoryUsage="Využitie pamäte" +Basic.Stats.AverageTimeToRender="Priemerný čas vykreslenia snímku" +Basic.Stats.SkippedFrames="Preskočené snímky kvôli chybe kódovania" +Basic.Stats.MissedFrames="Nevyužité snímky kvôli chybe vykresľovania" Basic.Stats.Output.Stream="Stream" Basic.Stats.Output.Recording="Nahrávanie" Basic.Stats.Status="Stav" @@ -151,15 +166,23 @@ Updater.UpdateNow="Aktualizovať teraz" Updater.RemindMeLater="Pripomenúť neskôr" Updater.Skip="Preskočiť verziu" Updater.Running.Title="Program momentálne aktívny" +Updater.Running.Text="Niektoré výstupy sú stále aktívne. Zastavte ich pred ďalším pokusom o aktualizáciu" Updater.NoUpdatesAvailable.Title="Neboli nájdené žiadne aktualizácie" Updater.NoUpdatesAvailable.Text="Nie sú momentálne k dispozícii žiadne aktualizácie" Updater.FailedToLaunch="Nepodarilo sa spustiť aktualizátor" Updater.GameCaptureActive.Title="Zachytenie hry aktívne" +Updater.GameCaptureActive.Text="Knižnica pre záznam hier je stále aktívna. Ukončite, prosím, všetky snímané hry/programy (alebo reštartujte Windows) a skúste to znovu." +QuickTransitions.SwapScenes="Prehodiť scény po prechode" +QuickTransitions.SwapScenesTT="Prehodí scény pre náhľad a výstup po prechode (ak pôvodná scéna výstupu stále existuje).\nToto nevráti žiadne zmeny vykonané v pôvodnej scéne pre výstup." QuickTransitions.DuplicateScene="Duplikovať scénu" +QuickTransitions.DuplicateSceneTT="Pri úprave rovnakej scény umožňuje úpravu pozície/viditeľnosti zdrojov bez úpravy výstupu.\nPre úpravu vlastností zdrojov bez úpravy výstupu povoľte možnosť \"Duplikovať zdroje\".\nZmena tejto hodnoty spôsobí vyresetovanie súčasnej scény výstupu (ak stále existuje)." QuickTransitions.EditProperties="Duplikovať zdroje" +QuickTransitions.EditPropertiesTT="Pri úprave rovnakej scény umožňuje nastavenie zdrojov bez úpravy výstupu.\nTáto funkcia môže byť použitá, len ak je zapnutá možnosť \"Duplikovať scénu\".\nNiektoré zdroje (napr. zdroje záznamu či mediálne zdroje) túto funkciu nepodporujú a nemôžu byť upravované samostatne.\nPo zmene tejto hodnoty bude súčasná scéna výstupu vyresetovaná (ak stále existuje).\n\nVarovanie: Z dôvodu duplikácie zdrojov môže táto funkcia vyžadovať viac systémových prostriedkov." QuickTransitions.HotkeyName="Rýchly prechod: %1" +Basic.AddTransition="Pridať nastaviteľný prechod" +Basic.RemoveTransition="Odstrániť nastaviteľný prechod" Basic.TransitionProperties="Vlastnosti prechodu" Basic.SceneTransitions="Prechody medzi scénami" Basic.TransitionDuration="Trvanie" @@ -193,56 +216,85 @@ ConfirmRemove.TextMultiple="Naozaj si prajete odstrániť %1 položiek?" Output.StartStreamFailed="Nepodarilo sa spustiť streamovanie" Output.StartRecordingFailed="Nepodarilo sa spustiť nahrávanie" Output.StartReplayFailed="Nepodarilo sa spustiť medzipamäť znovunahrávania" +Output.StartFailedGeneric="Nastala chyba pri spúšťaní nahrávania. Podrobnosti nájdete v textovom logu.\n\nPoznámka: Ak používate NVENC alebo AMD enkodér, uistite sa, že používate najnovšiu verziu grafického ovládača." Output.ConnectFail.Title="Spojenie sa nepodarilo" Output.ConnectFail.BadPath="Neplatná cesta alebo URL. Prosím, skontrolujte, či sú vaše nastavenia správne." Output.ConnectFail.ConnectFailed="Spojenie so serverom sa nepodarilo" +Output.ConnectFail.InvalidStream="K nastavenému kanálu alebo kľúču sa nedá pristupovať. Skontrolujte, prosím, či je váš vysielací kľúč správny. Ak áno, mohol nastať problém s pripojením k serveru." +Output.ConnectFail.Error="Pri pokuse o pripojenie k serveru nastala neočakávaná chyba. Ďalšie informácie nájdete v textovom logu." Output.ConnectFail.Disconnected="Odpojený od servera." Output.RecordFail.Title="Nepodarilo sa spustiť nahrávanie" +Output.RecordFail.Unsupported="Výstupný formát nie je podporovaný alebo nepodporuje viac ako jednu zvukovú stopu. Skontrolujte nastavenia a skúste to znovu." Output.RecordNoSpace.Title="Nedostatok miesta na disku" +Output.RecordNoSpace.Msg="Pre pokračovanie nahrávania nie je dostatok miesta na disku." Output.RecordError.Title="Chyba nahrávania" +Output.RecordError.Msg="Pri nahrávaní došlo k nešpecifikovanej chybe." Output.ReplayBuffer.NoHotkey.Title="Nepriradená žiadna klávesová skratka!" +Output.ReplayBuffer.NoHotkey.Msg="Nie je nastavená žiadna klávesová skratka pre uloženie záznamu. Nastavte ju, prosím, aby ste mohli ukladať záznam." Output.BadPath.Title="Nesprávna cesta k súboru" +Output.BadPath.Text="Nastavená cesta k výstupnému súboru je chybná. Prosím, skontrolujte správnosť nastavenej cesty." LogReturnDialog="Nahranie logu bolo úspešné" LogReturnDialog.CopyURL="Kopírovať URL" LogReturnDialog.ErrorUploadingLog="Chyba pri nahrávaní súboru denníka chýb" LicenseAgreement="Licenčná zmluva" +LicenseAgreement.PleaseReview="Pred použitím OBS si, prosím, prečítajte licenčné podmienky. Používaním tohto programu potvrdzujete, že ste si prečítali a súhlasíte s podmienkami GNU General Public License v2.0. Pre zobrazenie zvyšku licencie prejdite nižšie." +LicenseAgreement.ClickIAgreeToContinue="Ak súhlasíte s podmienkami licencie, kliknite na Súhlasím. Pred začatím používania OBS musíte súhlasiť s ujednaním." LicenseAgreement.IAgree="Súhlasím" LicenseAgreement.Exit="Ukončiť" Remux.SourceFile="OBS nahrávka" Remux.TargetFile="Cieľový súbor" +Remux.Remux="Previesť" Remux.OBSRecording="OBS nahrávanie" +Remux.FinishedTitle="Prevod dokončený" +Remux.Finished="Nahrávka prevedená" +Remux.FinishedError="Nahrávka prevedená, ale súbor nemusí byť kompletný" Remux.SelectRecording="Vybrať OBS nahrávku …" Remux.SelectTarget="Vyberte cieľový súbor …" Remux.FileExistsTitle="Cieľový súbor existuje" Remux.FileExists="Cieľový súbor už existuje, chcete ho nahradiť?" +Remux.ExitUnfinishedTitle="Prebieha prevod" +Remux.ExitUnfinished="Prevod nie je dokončený. Zastavenie môže spôsobiť poškodenie výstupného súboru.\nNaozaj chcete prevod prerušiť?" UpdateAvailable="Je dostupná aktualizácia" UpdateAvailable.Text="Verzia %1.%2.%3 je dostupná. Kliknite sem na jej stiahnutie" +Basic.DesktopDevice1="Zvuk plochy" +Basic.DesktopDevice2="Zvuk plochy 2" Basic.AuxDevice1="Mik/Aux" Basic.AuxDevice2="Mik/Aux 2" +Basic.AuxDevice3="Mikrofon / AUX 3" +Basic.AuxDevice4="Mikrofon / AUX 4" Basic.Scene="Scéna" Basic.DisplayCapture="Zachytávanie monitora" Basic.Main.PreviewConextMenu.Enable="Zapnúť náhľad" +ScaleFiltering="Filter škálovania" ScaleFiltering.Point="Bodové" ScaleFiltering.Bilinear="Bilineárne" ScaleFiltering.Bicubic="Bikubické" ScaleFiltering.Lanczos="Lanczos" Deinterlacing="Odstránenie prekladania" +Deinterlacing.Discard="Zahodenie" Deinterlacing.Retro="Retro" +Deinterlacing.Blend="Prechod" +Deinterlacing.Blend2x="Prechod 2x" +Deinterlacing.Linear="Lineárny" +Deinterlacing.Linear2x="Lineárny 2x" Deinterlacing.Yadif="Yadif" Deinterlacing.Yadif2x="Yadif 2x" +Deinterlacing.TopFieldFirst="Najprv vrchný riadok" +Deinterlacing.BottomFieldFirst="Najprv spodný riadok" +VolControl.SliderUnmuted="Posuvník hlasitosti pre '%1': %2" VolControl.SliderMuted="Ovládanie hlasitosti pre '%1': %2 (momentálne umlčané)" VolControl.Mute="Umlčať '%1'" VolControl.Properties="Vlastnosti pre '%1'" @@ -252,12 +304,18 @@ Basic.Main.AddSceneDlg.Text="Prosím, zadajte názov scény" Basic.Main.DefaultSceneName.Text="Scéna %1" +Basic.Main.AddSceneCollection.Title="Pridať sadu scén" +Basic.Main.AddSceneCollection.Text="Prosím, zadajte názov sady scén" +Basic.Main.RenameSceneCollection.Title="Premenovať sadu scén" AddProfile.Title="Pridanie profilu" +AddProfile.Text="Prosím, zadajte názov profilu" RenameProfile.Title="Premenovať profil" +Basic.Main.MixerRename.Title="Premenovať zdroj zvuku" +Basic.Main.MixerRename.Text="Prosím, zadajte názov zdroja zvuku" Basic.Main.PreviewDisabled="Náhľad je momentálne vypnutý" @@ -265,6 +323,7 @@ Basic.Main.PreviewDisabled="Náhľad je momentálne vypnutý" Basic.SourceSelect="Vytvoriť/Vybrať zdroj" Basic.SourceSelect.CreateNew="Vytvoriť nový" Basic.SourceSelect.AddExisting="Pridať existujúci" +Basic.SourceSelect.AddVisible="Zviditeľniť zdroj" Basic.PropertiesWindow="Vlastnosti pre '%1'" Basic.PropertiesWindow.AutoSelectFormat="%1 (Automatický výber: %2)" @@ -279,8 +338,11 @@ Basic.PropertiesWindow.AddURL="Pridať cestu/URL" Basic.PropertiesWindow.AddEditableListDir="Pridať adresár do '%1'" Basic.PropertiesWindow.AddEditableListFiles="Pridať súbory do '%1'" Basic.PropertiesWindow.AddEditableListEntry="Pridať položku do '%1'" +Basic.PropertiesWindow.EditEditableListEntry="Upraviť položku z '%1'" Basic.PropertiesView.FPS.Simple="Jednoduché hodnoty FPS" +Basic.PropertiesView.FPS.Rational="Rozumné hodnoty FPS" +Basic.PropertiesView.FPS.ValidFPSRanges="Platné rozsahy FPS:" Basic.InteractionWindow="V interakcii s '%1'" @@ -295,6 +357,7 @@ Basic.StatusBar.DelayStartingStoppingIn="Oneskorenie (zastavenie za %1 s, začí Basic.Filters="Filtre" Basic.Filters.AsyncFilters="Audio/Video filtre" Basic.Filters.AudioFilters="Audio filtre" +Basic.Filters.EffectFilters="Filtre efektov" Basic.Filters.Title="Filtre pre '%1'" Basic.Filters.AddFilter.Title="Názov filtra" Basic.Filters.AddFilter.Text="Prosím, zadajte názov filtra" @@ -335,16 +398,24 @@ Basic.Main.Sources="Zdroje" Basic.Main.Controls="Ovládacie prvky" Basic.Main.Connecting="Pripájanie..." Basic.Main.StartRecording="Spustiť nahrávanie" +Basic.Main.StartReplayBuffer="Spustiť Replay Buffer" Basic.Main.StartStreaming="Spustiť stream" Basic.Main.StopRecording="Ukončiť nahrávanie" Basic.Main.StoppingRecording="Zastavenie nahrávania..." +Basic.Main.StopReplayBuffer="Zastaviť Replay Buffer" +Basic.Main.StoppingReplayBuffer="Zastavujem Replay Buffer..." Basic.Main.StopStreaming="Ukončiť stream" Basic.Main.StoppingStreaming="Zastavenie streamu..." +Basic.Main.ForceStopStreaming="Zastaviť vysielanie (bez oneskorenia)" +Basic.Main.Group="Skupina %1" +Basic.Main.GroupItems="Zoskupiť vybrané položky" +Basic.Main.Ungroup="Rozdeliť skupinu" Basic.MainMenu.File="Súbor (&F)" Basic.MainMenu.File.Export="&Exportovať" Basic.MainMenu.File.Import="&Importovať" Basic.MainMenu.File.ShowRecordings="Zobraziť nah&rávky" +Basic.MainMenu.File.Remux="Previesť &nahrávky" Basic.MainMenu.File.Settings="Na&stavenia" Basic.MainMenu.File.ShowSettingsFolder="Zobraziť priečinok nastavení" Basic.MainMenu.File.ShowProfileFolder="Zobraziť priečinok profilu" @@ -357,6 +428,8 @@ Basic.MainMenu.Edit.Redo="Opakovať v&rátené" Basic.MainMenu.Edit.UndoAction="Vrátiť $1 (&U)" Basic.MainMenu.Edit.RedoAction="Opakovať $1 (&R)" Basic.MainMenu.Edit.LockPreview="&Zámknúť náhľad" +Basic.MainMenu.Edit.Scale="%Rozmer náhľadu" +Basic.MainMenu.Edit.Scale.Window="Vtesnať do okna" Basic.MainMenu.Edit.Scale.Canvas="Plátno (%1x%2)" Basic.MainMenu.Edit.Scale.Output="Výstup (%1x%2)" Basic.MainMenu.Edit.Transform="&Transformácia" @@ -377,24 +450,42 @@ Basic.MainMenu.Edit.Order.MoveUp="Pos&unúť vyššie" Basic.MainMenu.Edit.Order.MoveDown="Posunúť nižšie (&D)" Basic.MainMenu.Edit.Order.MoveToTop="Premies&tniť navrch" Basic.MainMenu.Edit.Order.MoveToBottom="Premiestniť naspodok (&B)" +Basic.MainMenu.Edit.AdvAudio="Rozšírené vl&astnosti zvuku" +Basic.MainMenu.View="Zobraziť (&V)" Basic.MainMenu.View.Toolbars="&Panely s nástrojmi" Basic.MainMenu.View.Docks="Doky" Basic.MainMenu.View.Docks.ResetUI="Resetovať UI" Basic.MainMenu.View.Docks.LockUI="Zamknúť UI" +Basic.MainMenu.View.Toolbars.Listboxes="Zoznamy (&L)" +Basic.MainMenu.View.SceneTransitions="Pre&chody scén" +Basic.MainMenu.View.StatusBar="&Stavový riadok" +Basic.MainMenu.View.Fullscreen.Interface="Na celú obrazovku" +Basic.MainMenu.SceneCollection="&Sada scén" Basic.MainMenu.Profile="&Profil" Basic.MainMenu.Profile.Import="Importovať profil" Basic.MainMenu.Profile.Export="Exportovať profil" +Basic.MainMenu.SceneCollection.Import="Importovať sadu scén" +Basic.MainMenu.SceneCollection.Export="Exportovať sadu scén" Basic.MainMenu.Profile.Exists="Tento profil už existuje" +Basic.MainMenu.SceneCollection.Exists="Táto sada scén už existuje" Basic.MainMenu.Tools="Nás&troje" Basic.MainMenu.Help="Pomoc (&H)" +Basic.MainMenu.Help.HelpPortal="&Portál pomoci" Basic.MainMenu.Help.Website="Navštíviť &webové stránky" +Basic.MainMenu.Help.Discord="Pripojiť sa na &Discord server" Basic.MainMenu.Help.Logs="&Log súbory" Basic.MainMenu.Help.Logs.ShowLogs="Zobraziť log &súbory" +Basic.MainMenu.Help.Logs.UploadCurrentLog="Nahrať súčasný súbor záznamu (&C)" +Basic.MainMenu.Help.Logs.UploadLastLog="Nahrať posledný súbor záznamu (&L)" +Basic.MainMenu.Help.Logs.ViewCurrentLog="Zobraziť aktuálny záznam (&V)" Basic.MainMenu.Help.CheckForUpdates="Skontrolovať aktualizácie" +Basic.MainMenu.Help.CrashLogs="&Hlásenia o zlyhaní" +Basic.MainMenu.Help.CrashLogs.ShowLogs="&Zobraziť správy o zlyhaniach" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="&Nahrať poslednú správu" Basic.Settings.ProgramRestart="Tieto nastavenia sa prejavia až po reštarte programu." Basic.Settings.ConfirmTitle="Potvrdenie zmien" @@ -403,11 +494,23 @@ Basic.Settings.Confirm="Máte neuložené zmeny. Chcete uložiť zmeny?" Basic.Settings.General="Všeobecné" Basic.Settings.General.Theme="Vzhľad" Basic.Settings.General.Language="Jazyk" +Basic.Settings.General.EnableAutoUpdates="Automaticky kontrolovať aktualizácie pri štarte" Basic.Settings.General.Projectors="Projektory" +Basic.Settings.General.HideProjectorCursor="Skryť kurzor pred náhľadmi" +Basic.Settings.General.ProjectorAlwaysOnTop="Náhľady vždy navrchu" +Basic.Settings.General.Snapping="Prichytávanie zdrojov" +Basic.Settings.General.ScreenSnapping="Prichytávať zdroje k okraju obrazovky" +Basic.Settings.General.CenterSnapping="Prichytávať zdroje ku stredu vodorovnej a zvislej osi" +Basic.Settings.General.SourceSnapping="Prichytávať zdroje k iným zdrojom" +Basic.Settings.General.SnapDistance="Prichytávať citlivosť" +Basic.Settings.General.RecordWhenStreaming="Automaticky nahrávať pri vysielaní" +Basic.Settings.General.KeepRecordingWhenStreamStops="Nahrávať aj po ukončení vysielania" Basic.Settings.General.SysTray="Systémová lišta" Basic.Settings.General.SysTrayWhenStarted="Minimalizovať do systémovej lišty pri spustení" Basic.Settings.General.SystemTrayHideMinimize="Vždy minimalizovať do systémovej lišty namiesto panela úloh" Basic.Settings.General.SaveProjectors="Uložiť projektory pri skončení" +Basic.Settings.General.Multiview.DrawSourceNames="Zobraziť názvy scén" +Basic.Settings.General.Multiview.DrawSafeAreas="Vykresliť bezpečné zóny (EBU R 95)" Basic.Settings.Stream="Stream" Basic.Settings.Stream.StreamType="Typ streamu" @@ -422,32 +525,48 @@ Basic.Settings.Output.Mode="Režim výstupu" Basic.Settings.Output.Mode.Simple="Jednoduchý" Basic.Settings.Output.Mode.Adv="Rozšírené" Basic.Settings.Output.Mode.FFmpeg="Výstup FFmpeg" +Basic.Settings.Output.ReplayBuffer.Estimate="Odhadované využitie pamäte: %1 MB" Basic.Settings.Output.ReplayBuffer.Suffix="Prípona" +Basic.Settings.Output.Simple.SavePath="Nahrávacia cesta" Basic.Settings.Output.Simple.RecordingQuality="Kvalita nahrávania" Basic.Settings.Output.Simple.RecordingQuality.Stream="Rovnaká ako pre stream" Basic.Settings.Output.Simple.RecordingQuality.Small="Vysoká kvalita, stredná veľkosť súboru" Basic.Settings.Output.Simple.RecordingQuality.Lossless="Bezstratová kvalita, ohromne veľké súbory" -Basic.Settings.Output.Simple.Encoder.Software="Softvér (x264)" -Basic.Settings.Output.Simple.Encoder.Hardware.QSV="Hardvér (QSV)" -Basic.Settings.Output.Simple.Encoder.Hardware.AMD="Hardvér (AMD)" +Basic.Settings.Output.Simple.Warn.VideoBitrate="Upozornenie: Dátový tok vysielaného obrazu bude nastavený na %1, čo je horná hranica pre súčasnú vysielaciu službu. Ak ste si istý, že chcete vysielať nad %1, v rozšírených nastaveniach enkodéra zakážte \"Vynútiť limit dátového toku vysielacou službou\"." +Basic.Settings.Output.Simple.Warn.AudioBitrate="Upozornenie: Dátový tok vysielaného zvuku bude nastavený na %1, čo je horná hranica pre súčasnú vysielaciu službu. Ak ste si istý, že chcete vysielať nad %1, v rozšírených nastaveniach enkodéra zakážte \"Vynútiť limit dátového toku vysielacou službou\"." +Basic.Settings.Output.Simple.Warn.Encoder="Upozornenie: Nahrávanie softvérovým enkodérom s rozdielnou kvalitou než vysielanie spôsobí zvýšenú záťaž CPU pri nahrávaní a vysielaní zároveň." +Basic.Settings.Output.Simple.Warn.MultipleQSV="Upozornenie: Nemôžete použiť viacero QSV kodérov pri vysielaní a nahrávaní zároveň. Ak chcete vysielať a nahrávať zároveň, zmeňte, prosím, kodér vysielania alebo kodér nahrávania." +Basic.Settings.Output.Simple.Encoder.Software="Softvérový (x264)" +Basic.Settings.Output.Simple.Encoder.Hardware.QSV="Hardvérový (QSV)" +Basic.Settings.Output.Simple.Encoder.Hardware.AMD="Hardvérový (AMD)" Basic.Settings.Output.Simple.Encoder.Hardware.NVENC="Hardvér (NVENC)" -Basic.Settings.Output.Simple.Encoder.SoftwareLowCPU="Softvér (x264 prednastavené nízke zaťaženie CPU, zvyšuje veľkosť súboru)" +Basic.Settings.Output.Simple.Encoder.SoftwareLowCPU="Softvérový (x264, nízke zaťaženie CPU, zvyšuje veľkosť súboru)" Basic.Settings.Output.VideoBitrate="Bitrate videa" Basic.Settings.Output.AudioBitrate="Bitrate zvuku" Basic.Settings.Output.Reconnect="Automaticky znovupripájať" Basic.Settings.Output.RetryDelay="Čas medzi pokusmi (sekundy)" Basic.Settings.Output.MaxRetries="Maximálny počet pokusov" Basic.Settings.Output.Advanced="Povoliť pokročilé nastavenia enkodéra" +Basic.Settings.Output.EncoderPreset="Predvoľba enkodéra (vyššie = menej CPU)" +Basic.Settings.Output.CustomEncoderSettings="Vlastné nastavenie enkodéra" +Basic.Settings.Output.Adv.Rescale="Škálovať výstup" +Basic.Settings.Output.Adv.AudioTrack="Zvuková stopa" +Basic.Settings.Output.Adv.Streaming="Vysielanie" +Basic.Settings.Output.Adv.ApplyServiceSettings="Vynútiť nastavenia kodéra vysielacou službou" Basic.Settings.Output.Adv.Audio.Track1="Stopa 1" Basic.Settings.Output.Adv.Audio.Track2="Stopa 2" Basic.Settings.Output.Adv.Audio.Track3="Stopa 3" Basic.Settings.Output.Adv.Audio.Track4="Stopa 4" +Basic.Settings.Output.Adv.Audio.Track5="Stopa 5" +Basic.Settings.Output.Adv.Audio.Track6="Stopa 6" Basic.Settings.Output.Adv.Recording="Nahrávanie" Basic.Settings.Output.Adv.Recording.Type="Typ" Basic.Settings.Output.Adv.Recording.Type.Standard="Štandardný" Basic.Settings.Output.Adv.Recording.Type.FFmpegOutput="Vlastný výstup (FFmpeg)" +Basic.Settings.Output.Adv.Recording.UseStreamEncoder="(Použiť kóder vysielania)" +Basic.Settings.Output.Adv.Recording.Filename="Formát názvu súboru" Basic.Settings.Output.Adv.Recording.OverwriteIfExists="Prepísať, ak súbor existuje" Basic.Settings.Output.Adv.FFmpeg.Type="Typ výstupu FFmpeg" Basic.Settings.Output.Adv.FFmpeg.Type.URL="Výstup na URL" @@ -455,18 +574,30 @@ Basic.Settings.Output.Adv.FFmpeg.Type.RecordToFile="Výstup do súboru" Basic.Settings.Output.Adv.FFmpeg.SaveFilter.Common="Bežné formáty nahrávania" Basic.Settings.Output.Adv.FFmpeg.SaveFilter.All="Všetky súbory" Basic.Settings.Output.Adv.FFmpeg.SavePathURL="Cesta k súboru alebo URL" +Basic.Settings.Output.Adv.FFmpeg.Format="Formát kontajneru" Basic.Settings.Output.Adv.FFmpeg.FormatAudio="Zvuk" Basic.Settings.Output.Adv.FFmpeg.FormatVideo="Video" Basic.Settings.Output.Adv.FFmpeg.FormatDefault="Predvolený formát" +Basic.Settings.Output.Adv.FFmpeg.FormatDesc="Popis formátu" +Basic.Settings.Output.Adv.FFmpeg.FormatDescDef="Kodér zvuku/obrazu uhádnutý z cesty súboru alebo URL adresy" Basic.Settings.Output.Adv.FFmpeg.AVEncoderDefault="Predvolený enkodér" Basic.Settings.Output.Adv.FFmpeg.AVEncoderDisable="Vypnúť enkodér" -Basic.Settings.Output.Adv.FFmpeg.VEncoder="Video Enkodér" -Basic.Settings.Output.Adv.FFmpeg.AEncoder="Audio Enkodér" +Basic.Settings.Output.Adv.FFmpeg.VEncoder="Kodér obrazu" +Basic.Settings.Output.Adv.FFmpeg.VEncoderSettings="Nastavenie kodéra obrazu (ak existuje)" +Basic.Settings.Output.Adv.FFmpeg.AEncoder="Kodér zvuku" +Basic.Settings.Output.Adv.FFmpeg.AEncoderSettings="Nastavenie kodéra zvuku (ak existuje)" +Basic.Settings.Output.Adv.FFmpeg.MuxerSettings="Nastavenie zmiešavača (ak existuje)" +Basic.Settings.Output.Adv.FFmpeg.GOPSize="Interval kľúčových snímkov (snímky)" +Basic.Settings.Output.Adv.FFmpeg.IgnoreCodecCompat="Zobraziť všetky kodeky (aj potenciálne nekompatibilné)" +FilenameFormatting.TT="%CCYY Rok, 4 číslice\n%YY Rok, posledné dve cifry (00-99)\n%MM Mesiac, číslo (01-12)\n%DD Deň v mesiaci, s úvodnou nulou (01-31)\n%hh Hodina vo 24-hod. formáte (00-23)\n%mm Minúta (00-59)\n%ss Sekunda (00-61)\n%% Znak %\n%a Skrátený názov dňa v týždni\n%A Celý názov dňa v týždni\n%b Skrátený názov mesiaca\n%B Celý názov mesiaca\n%d Deň v mesiaci, s úvodnou nulou (01-31)\n%H Hodina v 24-hod. formáte (00-23)\n%I Hodina v 12-hod. formáte (01-12)\n%m Mesiac ako desiatkové číslo (01-12)\n%M Minúta (00-59)\n%p Časť dňa (AM/PM)\n%S Sekunda (00-61)\n%y Rok, posledné dve cifry (00-99)\n%Y Rok\n%z ISO 8601 časový posun od UTC\n%Z Názov alebo skratka časového pásma\n" Basic.Settings.Video="Video" Basic.Settings.Video.Adapter="Video adaptér" +Basic.Settings.Video.BaseResolution="Základné rozlíšenie (plátno)" +Basic.Settings.Video.ScaledResolution="Výstupné (škálované) rozlíšenie" +Basic.Settings.Video.DownscaleFilter="Zmenšovací filter" Basic.Settings.Video.DisableAeroWindows="Vypnúť Aero (len Windows)" Basic.Settings.Video.FPS="FPS" Basic.Settings.Video.FPSCommon="Bežné hodnoty FPS" @@ -478,31 +609,60 @@ Basic.Settings.Video.InvalidResolution="Neplatné rozlíšenie. Správne je [š Basic.Settings.Video.CurrentlyActive="Výstup videa je práve aktívny. Prosím, vypnite všetky výstupy na zmenu nastavení videa." Basic.Settings.Video.DisableAero="Vypnúť Aero" +Basic.Settings.Video.DownscaleFilter.Bilinear="Bilineárne (Najrýchlejšie, ale rozmazané pri škálovaní)" +Basic.Settings.Video.DownscaleFilter.Bicubic="Bikubické (ostrejšie pri škálovaní, 16 vzoriek)" +Basic.Settings.Video.DownscaleFilter.Lanczos="Lanczosov (ostrejšie pri škálovaní, 32 vzoriek)" Basic.Settings.Audio="Zvuk" Basic.Settings.Audio.SampleRate="Vzorkovacia frekvencia" Basic.Settings.Audio.Channels="Kanály" +Basic.Settings.Audio.MeterDecayRate.Fast="Rýchlo" +Basic.Settings.Audio.MeterDecayRate.Medium="Stredne (typ I PPM)" +Basic.Settings.Audio.MeterDecayRate.Slow="Pomaly (typ II PPM)" +Basic.Settings.Audio.PeakMeterType="Typ merača špičiek" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Špička vzorky" +Basic.Settings.Audio.MultichannelWarning.Title="Povoliť priestorový zvuk?" +Basic.Settings.Audio.MultichannelWarning.Confirm="Naozaj si želáte povoliť priestorový zvuk?" +Basic.Settings.Audio.EnablePushToMute="Povoliť push-to-mute" +Basic.Settings.Audio.PushToMuteDelay="Oneskorenie push-to-mute" +Basic.Settings.Audio.EnablePushToTalk="Povoliť push-to-talk" +Basic.Settings.Audio.PushToTalkDelay="Oneskorenie push-to-talk" +Basic.Settings.Audio.UnknownAudioDevice="[Zariadenie nie je pripojené alebo dostupné]" +Basic.Settings.Advanced="Pokročilé" +Basic.Settings.Advanced.General.ProcessPriority="Priorita procesu" Basic.Settings.Advanced.General.ProcessPriority.High="Vysoká" Basic.Settings.Advanced.General.ProcessPriority.AboveNormal="Zvýšená" Basic.Settings.Advanced.General.ProcessPriority.Normal="Normálna" Basic.Settings.Advanced.General.ProcessPriority.BelowNormal="Znížená" Basic.Settings.Advanced.General.ProcessPriority.Idle="Nízka" +Basic.Settings.Advanced.FormatWarning="Upozornenie: Formáty farieb iné ako NV12 sú primárne určené na nahrávanie a nie sú odporúčane pre vysielanie. Použitie iných formátov môže spôsobiť zvýšené využitie CPU pri vysielaní kvôli prevodu medzi formátmi." +Basic.Settings.Advanced.Audio.BufferingTime="Čas medzipamäte zvuku" +Basic.Settings.Advanced.Video.ColorFormat="Formát farieb" Basic.Settings.Advanced.Video.ColorSpace="Farebný priestor YUV" Basic.Settings.Advanced.Video.ColorRange="Rozsah farieb YUV" Basic.Settings.Advanced.Video.ColorRange.Partial="Čiastočný" Basic.Settings.Advanced.Video.ColorRange.Full="Úplný" Basic.Settings.Advanced.Audio.MonitoringDevice.Default="Predvolené" +Basic.Settings.Advanced.StreamDelay="Oneskorenie vysielania" +Basic.Settings.Advanced.StreamDelay.Duration="Trvanie (sekundy)" +Basic.Settings.Advanced.StreamDelay.MemoryUsage="Odhadované využitie pamäte: %1 MB" +Basic.Settings.Advanced.Network="Sieť" Basic.Settings.Advanced.Network.BindToIP="Zviazať s IP adresou" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Použiť nový sieťový kód" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Režim nízkej odozvy" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Zakázať klávesové skratky, keď je hlavné okno aktívne" +Basic.AdvAudio="Pokročilé nastavenia zvuku" Basic.AdvAudio.Name="Názov" Basic.AdvAudio.Volume="Hlasitosť (%)" Basic.AdvAudio.Mono="Previesť na Mono" +Basic.AdvAudio.AudioTracks="Stopy" Basic.Settings.Hotkeys="Klávesové skratky" +Basic.Settings.Hotkeys.Pair="Klávesové skratky pre \"%1\" slúžia ako prepínače" +Basic.Hotkeys.SelectScene="Prepnúť na scénu" Basic.SystemTray.Show="Ukázať" Basic.SystemTray.Hide="Skryť" @@ -531,9 +691,28 @@ Hotkeys.Super="Super" Hotkeys.Menu="Menu" Hotkeys.Space="Medzerník" Hotkeys.NumpadNum="Numerická klávesa %1" +Hotkeys.AppleKeypadNum="%1 (num. klávesnica)" +Hotkeys.AppleKeypadMultiply="* (num. klávesnica)" +Hotkeys.AppleKeypadDivide="/ (num. klávesnica)" +Hotkeys.AppleKeypadAdd="+ (num. klávesnica)" +Hotkeys.AppleKeypadSubtract="- (num. klávesnica)" +Hotkeys.AppleKeypadDecimal=". (num. klávesnica)" +Hotkeys.AppleKeypadEqual="= (num. klávesnica)" +Hotkeys.MouseButton="Tlačidlo myši %1" +Mute="Stlmiť" +Unmute="Zrušiť stlmenie" FinalScene.Title="Odstrániť scénu" +FinalScene.Text="Musí existovať aspoň jedna scéna." + +NoSources.Title="Žiadne zdroje" +NoSources.Text="Nepridali ste žiadne zdroje obrazu, takže výstup bude prázdny. Naozaj chcete pokračovať?" +NoSources.Text.AddSource="Zdroje môžete kedykoľvek pridať kliknutím na ikonu + v zozname zdrojov v hlavnom okne." + +ChangeBG="Nastaviť farbu" +CustomColor="Vlastná farba" + diff --git a/UI/data/locale/sl-SI.ini b/UI/data/locale/sl-SI.ini index ea3a3bd..d749b2c 100644 --- a/UI/data/locale/sl-SI.ini +++ b/UI/data/locale/sl-SI.ini @@ -329,3 +329,6 @@ Basic.Settings.Advanced.Video.ColorRange.Full="Celotno" + + + diff --git a/UI/data/locale/sq-AL.ini b/UI/data/locale/sq-AL.ini index 8552391..7a88840 100644 --- a/UI/data/locale/sq-AL.ini +++ b/UI/data/locale/sq-AL.ini @@ -164,6 +164,9 @@ BandwidthTest.Region.Other="Të tjera" + + + diff --git a/UI/data/locale/sr-CS.ini b/UI/data/locale/sr-CS.ini index d7fecd4..44ea89e 100644 --- a/UI/data/locale/sr-CS.ini +++ b/UI/data/locale/sr-CS.ini @@ -562,3 +562,6 @@ OutputWarnings.NoTracksSelected="Morate odabrati makar jednu traku" OutputWarnings.MultiTrackRecording="Upozorenje: Određeni formati (kao što je FLV) ne podržavaju više traka po snimku" + + + diff --git a/UI/data/locale/sr-SP.ini b/UI/data/locale/sr-SP.ini index 45c7242..49b2f82 100644 --- a/UI/data/locale/sr-SP.ini +++ b/UI/data/locale/sr-SP.ini @@ -562,3 +562,6 @@ OutputWarnings.NoTracksSelected="Морате одабрати макар јед OutputWarnings.MultiTrackRecording="Упозорење: Одређени формати (као што је FLV) не подржавају више трака по снимку" + + + diff --git a/UI/data/locale/sv-SE.ini b/UI/data/locale/sv-SE.ini index 01e34e9..b1b065b 100644 --- a/UI/data/locale/sv-SE.ini +++ b/UI/data/locale/sv-SE.ini @@ -78,6 +78,8 @@ None="Ingen" StudioMode.Preview="Förhandsvisning" StudioMode.Program="Program" ShowInMultiview="Visa i multivy" +VerticalLayout="Vertikal layout" +Group="Grupp" AlreadyRunning.Title="OBS körs redan" AlreadyRunning.Text="OBS körs redan! Såvida du gjorde detta med flit, stäng ned alla befintliga instanser av OBS innan du försöker köra en ny instans. Om du har minimerat OBS till systemfältet, kontroller om det fortfarande körs där." @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="Stoppar reprisbuffert..." Basic.Main.StopStreaming="Sluta strömma" Basic.Main.StoppingStreaming="Stoppar ström..." Basic.Main.ForceStopStreaming="Sluta strömma (ignorera fördröjning)" +Basic.Main.Group="Grupp %1" +Basic.Main.GroupItems="Gruppmarkerade föremål" +Basic.Main.Ungroup="Avgruppera" Basic.MainMenu.File="&Arkiv" Basic.MainMenu.File.Export="&Exportera" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="&Verktyg" Basic.MainMenu.Help="&Hjälp" Basic.MainMenu.Help.HelpPortal="Hjälp&portal" Basic.MainMenu.Help.Website="Besök &webbplats" +Basic.MainMenu.Help.Discord="Anslut till &Discord-servern" Basic.MainMenu.Help.Logs="&Loggfiler" Basic.MainMenu.Help.Logs.ShowLogs="&Visa loggfiler" Basic.MainMenu.Help.Logs.UploadCurrentLog="Ladda upp &aktuell loggfil" Basic.MainMenu.Help.Logs.UploadLastLog="Ladda upp &senaste loggfil" Basic.MainMenu.Help.Logs.ViewCurrentLog="&Visa Aktuell Logg" Basic.MainMenu.Help.CheckForUpdates="Sök efter uppdateringar" +Basic.MainMenu.Help.CrashLogs="Krasch&rapporter" +Basic.MainMenu.Help.CrashLogs.ShowLogs="&Visa kraschrapporter" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="Ladda upp &senaste kraschrapport" Basic.Settings.ProgramRestart="Du måste starta om programmet för att ändringarna ska träda i kraft." Basic.Settings.ConfirmTitle="Bekräfta ändringar" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Minimera alltid till meddelandef Basic.Settings.General.SaveProjectors="Spara projektorer vid avslut" Basic.Settings.General.SwitchOnDoubleClick="Övergång till scen vid dubbelklick" Basic.Settings.General.StudioPortraitLayout="Aktivera porträtt-/vertikalt utseende" +Basic.Settings.General.Multiview="Multivy" +Basic.Settings.General.Multiview.MouseSwitch="Klicka för att byta mellan scener" +Basic.Settings.General.Multiview.DrawSourceNames="Visa scennamn" +Basic.Settings.General.Multiview.DrawSafeAreas="Rita ut säkra områden (EBU R 95)" Basic.Settings.General.MultiviewLayout="Utseende för multivy" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horisontal, överkant" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horisontal, nederkant" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertikal, vänster" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertikal, höger" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Horisontal, överkant (8 scener)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Horisontal, nederkant (8 scener)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Vertikal, vänsterkant (8 scener)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Vertikal, högerkant (8 scener)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Horisontal, överkant (24 scener)" Basic.Settings.Stream="Ström" Basic.Settings.Stream.StreamType="Strömtyp" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="Ljudmätarens förfallfrekvens" Basic.Settings.Audio.MeterDecayRate.Fast="Snabb" Basic.Settings.Audio.MeterDecayRate.Medium="Medium (Type I PPM)" Basic.Settings.Audio.MeterDecayRate.Slow="Långsam (Type II PPM)" +Basic.Settings.Audio.PeakMeterType="Typ av maxpunktsmätare" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Samplingsmaxpunkt" +Basic.Settings.Audio.PeakMeterType.TruePeak="Sann maxpunkt (högre CPU-användning)" Basic.Settings.Audio.MultiChannelWarning.Enabled="VARNING: Surroundljud är aktiverat." Basic.Settings.Audio.MultichannelWarning="Om du strömmar, se till att kolla om din strömtjänst stöder både inmatning och uppspelning av surroundljud. Twitch, Facebook 360 Live, Mixer RTMP, Smashcast är några exempel på tjänster som har fullt stöd. Fastän Facebook Live och YouTube Live stöder inmatning för surroundljud mixar Facebook Live ned till stereo och YouTube Live spelar upp i bara två kanaler.\n\nLjudfiltren i OBS är kompatibla med surroundljud, fast stöd för VST-insticksmodulen garanteras inte." Basic.Settings.Audio.MultichannelWarning.Title="Aktivera surroundljud?" @@ -661,7 +678,7 @@ Basic.Settings.Advanced.FormatWarning="Varning: Andra färgformat än NV12 är a Basic.Settings.Advanced.Audio.BufferingTime="Ljudbuffringstid" Basic.Settings.Advanced.Video.ColorFormat="Färgformat" Basic.Settings.Advanced.Video.ColorSpace="YUV-färgrymd" -Basic.Settings.Advanced.Video.ColorRange="YUV färgområde" +Basic.Settings.Advanced.Video.ColorRange="YUV-färgområde" Basic.Settings.Advanced.Video.ColorRange.Partial="Partiell" Basic.Settings.Advanced.Video.ColorRange.Full="Full" Basic.Settings.Advanced.Audio.MonitoringDevice="Ljuduppspelningsenhet" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="Nätverk" Basic.Settings.Advanced.Network.BindToIP="Bind till IP" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Aktivera ny nätverkskod" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Låg latens-läge" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Inaktivera kortkommandon när fokus ligger i huvudfönstret" Basic.AdvAudio="Avancerade ljudinställningar" Basic.AdvAudio.Name="Namn" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="Varning: Inspelningar som sparas som MP4 kommer int FinalScene.Title="Radera scen" FinalScene.Text="Det måste finnas minst en scen." +NoSources.Title="Inga källor" +NoSources.Text="Det verkar som om du inte har lagt till några videokällor än, så du kommer endast att visa en tom skärm. Är du säker på att du vill göra detta?" +NoSources.Text.AddSource="Du kan lägga till källor genom att klicka på plusikonen under rutan \"källor\" i huvudfönstret när som helst." + +ChangeBG="Ändra färg" +CustomColor="Anpassad färg" + +BrowserSource.EnableHardwareAcceleration="Aktiverar webbläsarkällans hårdvaruaccelerering" + diff --git a/UI/data/locale/ta-IN.ini b/UI/data/locale/ta-IN.ini index a93f85a..b419738 100644 --- a/UI/data/locale/ta-IN.ini +++ b/UI/data/locale/ta-IN.ini @@ -100,6 +100,9 @@ New="புதிய" + + + diff --git a/UI/data/locale/th-TH.ini b/UI/data/locale/th-TH.ini index cbbb8e6..7ab922a 100644 --- a/UI/data/locale/th-TH.ini +++ b/UI/data/locale/th-TH.ini @@ -148,3 +148,6 @@ Basic.Settings.Audio="เสียง" + + + diff --git a/UI/data/locale/tl-PH.ini b/UI/data/locale/tl-PH.ini index 73be893..c4d5628 100644 --- a/UI/data/locale/tl-PH.ini +++ b/UI/data/locale/tl-PH.ini @@ -508,10 +508,6 @@ Basic.Settings.General.SaveProjectors="I-save ang mga projector sa paglabas" Basic.Settings.General.SwitchOnDoubleClick="Lumipat sa eksena kapag dalawang beses pinindot" Basic.Settings.General.StudioPortraitLayout="Paganahin ang portrait/patayong layout" Basic.Settings.General.MultiviewLayout="Multiview Layout" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Pahalang, Tuktok" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Pahalang, Pinaka baba" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Patayo, Kaliwa" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Patayo, Kanan" Basic.Settings.Stream="Mag-stream" Basic.Settings.Stream.StreamType="Uri ng Pag-stream" @@ -749,3 +745,6 @@ OutputWarnings.MP4Recording="Babala: Ang mga rekording na naka-save sa MP4 ay hi FinalScene.Title="Burahin ang Eksena" FinalScene.Text="Kailangan mayroon kahit isang eksena." + + + diff --git a/UI/data/locale/tr-TR.ini b/UI/data/locale/tr-TR.ini index dfc1200..7aee010 100644 --- a/UI/data/locale/tr-TR.ini +++ b/UI/data/locale/tr-TR.ini @@ -78,6 +78,8 @@ None="Hiçbiri" StudioMode.Preview="Önizleme" StudioMode.Program="Program" ShowInMultiview="Çoklu Ekranda Göster" +VerticalLayout="Dikey Düzen" +Group="Grup" AlreadyRunning.Title="OBS zaten çalışıyor" AlreadyRunning.Text="OBS zaten çalışıyor! Bunu yapmak istemediyseniz, lütfen yeni bir örneği çalıştırmayı denemeden önce varolan tüm OBS örneklerini kapatın. OBS'yi sistem tablasına küçülmesi için ayarladıysanız, lütfen hala çalışıp çalışmadığını görmek için orayı kontrol edin." @@ -209,7 +211,7 @@ ConfirmExit.Text="OBS şu anda etkin. Tüm yayınlar / kayıtlar kapatılacak. ConfirmRemove.Title="Kaldırmayı Onayla" ConfirmRemove.Text="'$1''i kaldırmak istediğinizden emin misiniz?" -ConfirmRemove.TextMultiple="%1 öğeyi kaldırmak istediğinizden emin misiniz?" +ConfirmRemove.TextMultiple="%1 ögeyi kaldırmak istediğinizden emin misiniz?" Output.StartStreamFailed="Yayın başlatılamadı" Output.StartRecordingFailed="Kayıt başlatılamadı" @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="Tekrar Oynatma Arabelleği Durduruluyor..." Basic.Main.StopStreaming="Yayını Durdur" Basic.Main.StoppingStreaming="Canlı Yayın Durduruluyor..." Basic.Main.ForceStopStreaming="Yayını Durdur (gecikmeyi yoksay)" +Basic.Main.Group="Grup %1" +Basic.Main.GroupItems="Seçilen Ögeleri Grupla" +Basic.Main.Ungroup="Grubu Çöz" Basic.MainMenu.File="&Dosya" Basic.MainMenu.File.Export="Dışa Aktar" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="&Araçlar" Basic.MainMenu.Help="&Yardım" Basic.MainMenu.Help.HelpPortal="Yardım &Portalı" Basic.MainMenu.Help.Website="&Siteyi Ziyaret Et" +Basic.MainMenu.Help.Discord="&Discord Sunucusuna Katıl" Basic.MainMenu.Help.Logs="&Günlük Dosyaları" Basic.MainMenu.Help.Logs.ShowLogs="&Günlük Dosyalarını Göster" Basic.MainMenu.Help.Logs.UploadCurrentLog="&Mevcut Günlük Dosyasını Karşıya Yükle" Basic.MainMenu.Help.Logs.UploadLastLog="&Son Günlük Dosyasını Karşıya Yükle" Basic.MainMenu.Help.Logs.ViewCurrentLog="&Şimdiki Günlüğü Göster" Basic.MainMenu.Help.CheckForUpdates="Güncellemeleri Denetle" +Basic.MainMenu.Help.CrashLogs="Çökme &Raporları" +Basic.MainMenu.Help.CrashLogs.ShowLogs="Çökme Raporlarını &Göster" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="&Son Çökme Raporunu Yükle" Basic.Settings.ProgramRestart="Programın, bu ayarların etkinleşmesi için yeniden başlatılması gerekir." Basic.Settings.ConfirmTitle="Değişiklikleri Onayla" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Her zaman görev çubuğu yerine Basic.Settings.General.SaveProjectors="Çıkışta projektörleri kaydet" Basic.Settings.General.SwitchOnDoubleClick="Çift tıklamada sahneye geçiş yap" Basic.Settings.General.StudioPortraitLayout="Dikey düzeni etkinleştir" +Basic.Settings.General.Multiview="Çoklu görüntü" +Basic.Settings.General.Multiview.MouseSwitch="Sahneler arası geçiş için tıkla" +Basic.Settings.General.Multiview.DrawSourceNames="Sahne adlarını göster" +Basic.Settings.General.Multiview.DrawSafeAreas="Güvenli alanları (EBU R 95) çiz" Basic.Settings.General.MultiviewLayout="Çoklu Görüntü Düzeni" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Yatay, Üst" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Yatay, Alt" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Dikey, Sol" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Dikey, Sağ" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Yatay, Üst (8 Sahne)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Yatay, Alt (8 Sahne)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Dikey, Sol (8 Sahne)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Dikey, Sağ (8 Sahne)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Yatay, Üst (24 Sahne)" Basic.Settings.Stream="Yayın" Basic.Settings.Stream.StreamType="Yayın Türü" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="Ses Ölçer Sönüm Hızı" Basic.Settings.Audio.MeterDecayRate.Fast="Hızlı" Basic.Settings.Audio.MeterDecayRate.Medium="Orta (Tür I PPM)" Basic.Settings.Audio.MeterDecayRate.Slow="Yavaş (Tür II PPM)" +Basic.Settings.Audio.PeakMeterType="Tepe Ölçer Türü" +Basic.Settings.Audio.PeakMeterType.SamplePeak="Örnek Tepe" +Basic.Settings.Audio.PeakMeterType.TruePeak="Gerçek Tepe (Daha yüksek CPU kullanımı)" Basic.Settings.Audio.MultiChannelWarning.Enabled="Uyarı: Surround ses etkin." Basic.Settings.Audio.MultichannelWarning="Yayın yapılıyorsa, yayın hizmetinizin hem surround ses alınımını hem de surround ses geri oynatımını desteklediğinden emin olun. Twitch, Facebook 360 Live, Karıştırıcı RTMP, Smashcast, surround sesin tam desteklendiği örneklerdir. Facebook Live'ın ve YouTube Live'ın her ikisi de surround alınımını desteklese de, Facebook Live stereo'ya indirger, ve YouTube Live sadece iki kanal oynatır.\n\nOBS ses filtreleri surround sesle uyumludur, ancak VST eklenti desteği kesin değildir." Basic.Settings.Audio.MultichannelWarning.Title="Surround ses etkinleştirilsin mi?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="Ağ" Basic.Settings.Advanced.Network.BindToIP="IP Bağla" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Yeni ağ kodunu etkinleştir" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Düşük gecike modu" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Ana pencere odaktayken kısayol tuşlarını devre dışı bırak" Basic.AdvAudio="Gelişmiş Ses Özellikleri" Basic.AdvAudio.Name="İsim" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="Uyarı: MP4'e kaydedilen kayıtlar eğer dosya sonl FinalScene.Title="Sahneyi Sil" FinalScene.Text="En az bir sahne olması gerekiyor." +NoSources.Title="Kaynak Yok" +NoSources.Text="Henüz hiç video kaynağı eklemediniz gibi görünüyor, bu yüzden sadece boş bir ekran çıktısı alacaksınız. Bunu yapmak istediğinize emin misiniz?" +NoSources.Text.AddSource="Ana pencerede Kaynaklar kutusundaki + simgesine tıklayarak istediğiniz zaman kaynak ekleyebilirsiniz." + +ChangeBG="Renk Ayarla" +CustomColor="Özel Renk" + +BrowserSource.EnableHardwareAcceleration="Tarayıcı Kaynak Donanım Hızlandırmasını Etkinleştir" + diff --git a/UI/data/locale/uk-UA.ini b/UI/data/locale/uk-UA.ini index f45890b..57ff3fc 100644 --- a/UI/data/locale/uk-UA.ini +++ b/UI/data/locale/uk-UA.ini @@ -78,6 +78,8 @@ None="Немає" StudioMode.Preview="вікно Перегляду" StudioMode.Program="Програма наживо" ShowInMultiview="Показувати у Мульти-перегляді" +VerticalLayout="Вертикальне компонування" +Group="Група" AlreadyRunning.Title="OBS вже виконується" AlreadyRunning.Text="OBS вже запущено! Тільки якщо ви дійсно не намагаєтесь цього зробити, будь ласка позакривайте всі відкриті OBS перед тим як запускати нову копію. Якщо OBS налаштовано згортатися в трей, перевірте чи не виконується він там й досі." @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="Буфер Повторів зупиняєть Basic.Main.StopStreaming="Закінчити трансляцію" Basic.Main.StoppingStreaming="Припинення трансляції..." Basic.Main.ForceStopStreaming="Закінчити трансляцію (миттєво)" +Basic.Main.Group="Група %1" +Basic.Main.GroupItems="Згрупувати вибрані елементи" +Basic.Main.Ungroup="Розгрупувати" Basic.MainMenu.File="&Файл" Basic.MainMenu.File.Export="&Експорт" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="Додаткові &засоби" Basic.MainMenu.Help="&Довідка" Basic.MainMenu.Help.HelpPortal="&Портал з допомоги" Basic.MainMenu.Help.Website="Відвідати &сайт" +Basic.MainMenu.Help.Discord="Приє&днатися до серверу Discord" Basic.MainMenu.Help.Logs="&Файли журналів" Basic.MainMenu.Help.Logs.ShowLogs="&Показати файли журналів" Basic.MainMenu.Help.Logs.UploadCurrentLog="Завантажити на сервер По&точний журнал" Basic.MainMenu.Help.Logs.UploadLastLog="Завантажити на сервер &Останній журнал" Basic.MainMenu.Help.Logs.ViewCurrentLog="П&ереглянути поточний журнал" Basic.MainMenu.Help.CheckForUpdates="Перевірити оновлення" +Basic.MainMenu.Help.CrashLogs="Звіти про &збої" +Basic.MainMenu.Help.CrashLogs.ShowLogs="П&оказати звіти про збої" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="Заванта&жити на сервер останній звіт про збій" Basic.Settings.ProgramRestart="Програма потребує перезапуску, щоб нові налаштування набрали сили." Basic.Settings.ConfirmTitle="Підтвердження змін" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="Згортати в трей за Basic.Settings.General.SaveProjectors="Зберегти налаштування режиму Проектор при виході" Basic.Settings.General.SwitchOnDoubleClick="Відео-перехід до сцени за подвійним клацанням" Basic.Settings.General.StudioPortraitLayout="Увімкнути портретне/вертикальне компонування" +Basic.Settings.General.Multiview="Мульти-перегляд" +Basic.Settings.General.Multiview.MouseSwitch="Переходити до сцени за клацанням миші" +Basic.Settings.General.Multiview.DrawSourceNames="Показувати назви сцен" +Basic.Settings.General.Multiview.DrawSafeAreas="Показати зони безпеки (EBU R 95)" Basic.Settings.General.MultiviewLayout="Мульти-перегляд компонування" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Горизонтально, зверху" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Горизонтально, внизу" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Вертикально, ліворуч" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Вертикально, праворуч" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="Горизонтально, зверху (8 сцен)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Горизонтально, знизу (8 сцен)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="Вертикально, зліва (8 сцен)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="Вертикально, праворуч (8 сцен)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Горизонтально, зверху (24 сцени)" Basic.Settings.Stream="Трансляція" Basic.Settings.Stream.StreamType="Тип Трансляції" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="Швидкість спаду індика Basic.Settings.Audio.MeterDecayRate.Fast="Швидко" Basic.Settings.Audio.MeterDecayRate.Medium="Середньо (PPM типу I)" Basic.Settings.Audio.MeterDecayRate.Slow="Повільно (PPM типу II)" +Basic.Settings.Audio.PeakMeterType="Тип вимірювача пікових значень" +Basic.Settings.Audio.PeakMeterType.SamplePeak="З точністю до вибірки" +Basic.Settings.Audio.PeakMeterType.TruePeak="Істинно-піковий (більше навантаження на ЦП)" Basic.Settings.Audio.MultiChannelWarning.Enabled="ПОПЕРЕДЖЕННЯ: Увімкнуто об'ємний звук." Basic.Settings.Audio.MultichannelWarning="Якщо робите трансляції, перевірте чи підтримує ваш сервіс трансляцій об'ємний звук на вході та виході. Twitch, Facebook 360 Live, Mixer RTMP, Smashcast - це приклади сервісів де об'ємний звук цілком підтримується. Навпаки, Facebook Live і YouTube Live обидва підтримують вхідний об'ємний звук, але Facebook Live мікшує його до стерео, а YouTube Live відтворює лише два канали.\n\nАудіо фільтри самої OBS сумісні з об'ємним звуком, хоча підтримку VST плагінами не гарантовано." Basic.Settings.Audio.MultichannelWarning.Title="Увімкнути об'ємний звук для виводу аудіо?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="Мережа" Basic.Settings.Advanced.Network.BindToIP="Прив'язати до адаптера (IP)" Basic.Settings.Advanced.Network.EnableNewSocketLoop="Увімкнути новий мережевий код" Basic.Settings.Advanced.Network.EnableLowLatencyMode="Режим з низькою затримкою" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="Відключати гарячі клавіші, коли головне вікно знаходиться у фокусі" Basic.AdvAudio="Розширені Налаштування Аудіо" Basic.AdvAudio.Name="Назва" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="Попередження: Запис в MP4 мож FinalScene.Title="Видалення сцени" FinalScene.Text="Повинна бути принаймні одна сцена." +NoSources.Title="Немає Джерел" +NoSources.Text="Схоже, що ви не ще додали будь-якого відео Джерела. Таким чином на виводі буде лише порожній екран. Ви справді бажаєте це зробити?" +NoSources.Text.AddSource="Ви можете будь-коли додати Джерела, натиснувши на значок (+) під панеллю Джерела в головному вікні." + +ChangeBG="Позначити кольором" +CustomColor="Особливий колір" + +BrowserSource.EnableHardwareAcceleration="Задіяти апаратне прискорення для джерела 'Браузер'" + diff --git a/UI/data/locale/ur-PK.ini b/UI/data/locale/ur-PK.ini new file mode 100644 index 0000000..d0afe8d --- /dev/null +++ b/UI/data/locale/ur-PK.ini @@ -0,0 +1,136 @@ + +Language="انگلش" +Region="متحدہ ریاستوں" + +OK="ٹھیک ہے" +Apply="لاگو کریں" +Cancel="منسوخ کریں" +Close="بند کریں" +Save="بچاؤ" +Discard="چھوڑ دو" +Disable="غیر فعال" +Yes="جی ہاں" +No="نہیں" +Add="جمع" +Remove="ہٹا دیں" +Rename="تبدیل کریں" +Interact="بات چیت" +Filters="فلٹرز" +Properties="خصوصیات" +MoveUp="اوپر جایے" +MoveDown="نیچے جائے" +Settings="ترتیبات" +Display="ڈسپلے" +Name="نام" +Exit="بند کریں" +Mixer="مکسر" +Browse="براؤز کریں" +Mono="مونو" +Stereo="سٹیریو" +DroppedFrames="کھوئے گئے فریم 1 فیصد(2 فیصد)" +StudioProgramProjector="پورے اسکرین پروجیکٹر (پروگرام)" +PreviewProjector="پورے اسکرین پروجیکٹر (پیش نظارہ)" +SceneProjector="پورے اسکرین پروجیکٹر (منظر)" +SourceProjector="پورے اسکرین پروجیکٹر (ماخذ)" +StudioProgramWindow="ونڈوز پروجیکٹر (پروگرام)" +PreviewWindow="ونڈوز پروجیکٹر (پیش نظارہ)" +SceneWindow="ہوا ہوا پروجیکٹر (منظر)" +SourceWindow="ونڈوز پروجیکٹر (ماخذ)" +MultiviewProjector="Multiview (پورے اسکرین)" +MultiviewWindowed="Multi view (دریچہ شدہ)" +Clear="واضح" +Revert="واپس" +Show="دکھائیں" +Hide="چھپائیں" +UnhideAll="سب کو چھوڑ دو" +Untitled="غیر خطاب یافتہ" +New="نیا" +Duplicate="دونا کرنا" +Enable="قابل کریں" +DisableOSXVSync="OSX V-Sync کو غیر فعال کریں" +ResetOSXVSyncOnExit="باہر نکلیں پرOSX V-Sync ری سیٹ کریں" +HighResourceUsage="انکوڈنگ زیادہ اوورلوڈ! ویڈیو ترتیبات کو تبدیل کرنے یا تیزی سے انکوڈنگ کے پیش سیٹ کا استعمال کرتے ہوئے غور کریں." +Transition="منتقلی" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/data/locale/vi-VN.ini b/UI/data/locale/vi-VN.ini index b4caec9..763874d 100644 --- a/UI/data/locale/vi-VN.ini +++ b/UI/data/locale/vi-VN.ini @@ -66,8 +66,10 @@ RemuxRecordings="Remux video" Next="Tiếp tục" Back="Quay lại" Defaults="Mặc định" +None="Không có" StudioMode.Preview="Xem trước" StudioMode.Program="Chương trình" +Group="Nhóm" AlreadyRunning.Title="OBS đã chạy" AlreadyRunning.Text="OBS đã chạy rồi! Trừ khi bạn muốn làm điều này, xin vui lòng tắt mọi chương trình hiện tại của OBS trước khi cố gắng chạy một chương trình mới. Nếu bạn có OBS thiết lập để thu nhỏ trên khay hệ thống, xin vui lòng kiểm tra để xem nếu nó vẫn đang chạy hay không." @@ -292,6 +294,8 @@ AddProfile.Text="Vui lòng nhập tên cấu hình" RenameProfile.Title="Đổi tên cấu hình" +Basic.Main.MixerRename.Title="Đổi tên nguồn âm thanh" +Basic.Main.MixerRename.Text="Hãy nhập tên nguồn âm thanh" Basic.Main.PreviewDisabled="Xem trước hiện đang vô hiệu hoá" @@ -383,6 +387,7 @@ Basic.Main.StoppingReplayBuffer="Đang dừng Replay Buffer..." Basic.Main.StopStreaming="Ngừng Stream" Basic.Main.StoppingStreaming="Đang dừng stream..." Basic.Main.ForceStopStreaming="Ngừng Stream (huỷ chậm trễ)" +Basic.Main.Group="Nhóm %1" Basic.MainMenu.File="&Tập tin" Basic.MainMenu.File.Export="&Xuất" @@ -472,10 +477,6 @@ Basic.Settings.General.SysTrayWhenStarted="Thu nhỏ về khay hệ thống khi Basic.Settings.General.SystemTrayHideMinimize="Luôn luôn thu nhỏ về khay hệ thống thay vì thanh tác vụ" Basic.Settings.General.StudioPortraitLayout="Bật bố cục theo chiều ngang/dọc" Basic.Settings.General.MultiviewLayout="Giao diện nhiều lớp" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="Ngang, Trên" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="Ngang, Dưới" -Basic.Settings.General.MultiviewLayout.Vertical.Left="Dọc, Trái" -Basic.Settings.General.MultiviewLayout.Vertical.Right="Dọc, Phải" Basic.Settings.Stream="Stream" Basic.Settings.Stream.StreamType="Kiểu Stream" @@ -697,3 +698,8 @@ OutputWarnings.MP4Recording="Chú ý: các bản ghi âm được lưu ở dạn FinalScene.Title="Xóa cảnh" FinalScene.Text="Cần có ít nhất một cảnh." + +ChangeBG="Thiết lập màu sắc" +CustomColor="Tùy chỉnh màu sắc" + + diff --git a/UI/data/locale/zh-CN.ini b/UI/data/locale/zh-CN.ini index 905bbc5..86fb246 100644 --- a/UI/data/locale/zh-CN.ini +++ b/UI/data/locale/zh-CN.ini @@ -28,11 +28,11 @@ Browse="浏览" Mono="单声道​" Stereo="立体声​​​" DroppedFrames="丢帧 %1 (%2%)" -StudioProgramProjector="全屏投影仪(程序)" +StudioProgramProjector="全屏投影仪(输出)" PreviewProjector="全屏投影仪(预览)" SceneProjector="全屏投影仪 (现场)" SourceProjector="全屏投影仪(源)" -StudioProgramWindow="窗口式投影仪 (程序)" +StudioProgramWindow="窗口式投影仪 (输出)" PreviewWindow="窗口化投影仪 (预览)" SceneWindow="窗口化投影仪 (场景)" SourceWindow="窗口化投影仪 (源)" @@ -49,7 +49,7 @@ Duplicate="复制(&D)" Enable="启用" DisableOSXVSync="禁用 OSX V-Sync" ResetOSXVSyncOnExit="退出时重置 OSX V-Sync" -HighResourceUsage="编码过载! 考虑下调低视频设置或使用更快的编码预设." +HighResourceUsage="编码过载! 请考虑降低视频设置或使用更快的编码预设" Transition="过渡动画" QuickTransitions="快速过渡动画" Left="左" @@ -61,7 +61,7 @@ Hours="小时" Minutes="分钟" Seconds="秒" Deprecated="不推荐使用" -ReplayBuffer="重拨缓存" +ReplayBuffer="回放缓存" Import="导入" Export="导出" Copy="复制" @@ -76,15 +76,17 @@ HideMixer="混合器中隐藏" TransitionOverride="过渡覆盖模式" None="无" StudioMode.Preview="预览" -StudioMode.Program="程序" +StudioMode.Program="输出" ShowInMultiview="多屏中显示" +VerticalLayout="垂直布局" +Group="分组" AlreadyRunning.Title="OBS 已在运行" AlreadyRunning.Text="OBS 已经在运行! 除非你想要这样做, 请在你运行一个新的 OBS 前, 关闭任何已经在运行的 OBS. 如果你有一个 OBS 设置最小化到系统托盘, 请检查他是否仍在运行." -AlreadyRunning.LaunchAnyway="无论如何启动" +AlreadyRunning.LaunchAnyway="始终启动" -Copy.Filters="复制筛选器" -Paste.Filters="粘贴筛选器" +Copy.Filters="复制滤镜" +Paste.Filters="粘贴滤镜" BandwidthTest.Region="区域" BandwidthTest.Region.US="美国" @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="正在停止回放缓存..." Basic.Main.StopStreaming="停止推流" Basic.Main.StoppingStreaming="停止推流..." Basic.Main.ForceStopStreaming="停止流 (放弃延迟)" +Basic.Main.Group="分组 %1" +Basic.Main.GroupItems="对所选项目进行分组" +Basic.Main.Ungroup="取消分组" Basic.MainMenu.File="文件(&F)" Basic.MainMenu.File.Export="导出(&E)" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="工具 (&T)" Basic.MainMenu.Help="帮助 (&H)" Basic.MainMenu.Help.HelpPortal="帮助门户" Basic.MainMenu.Help.Website="访问OBS主页 (&W)" +Basic.MainMenu.Help.Discord="加入 &Discord 服务器" Basic.MainMenu.Help.Logs="日志文件 (&L)" Basic.MainMenu.Help.Logs.ShowLogs="显示日志文件 (&S)" Basic.MainMenu.Help.Logs.UploadCurrentLog="上传当前日志文件 (&C)" Basic.MainMenu.Help.Logs.UploadLastLog="上传最后一个日志文件 (&L)" Basic.MainMenu.Help.Logs.ViewCurrentLog="查看当前日志 (&V)" Basic.MainMenu.Help.CheckForUpdates="检查升级(&C)" +Basic.MainMenu.Help.CrashLogs="错误报告(&R)" +Basic.MainMenu.Help.CrashLogs.ShowLogs="查看错误报告(&S)" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="上传上一次错误报告(&L)" Basic.Settings.ProgramRestart="要使这些设置生效,必须重新启动该程序。" Basic.Settings.ConfirmTitle="确认更改" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="总是最小化到系统托盘, Basic.Settings.General.SaveProjectors="退出时保存投影仪" Basic.Settings.General.SwitchOnDoubleClick="双击时切换到场景" Basic.Settings.General.StudioPortraitLayout="启用纵向布局" +Basic.Settings.General.Multiview="多视图" +Basic.Settings.General.Multiview.MouseSwitch="点击切换场景" +Basic.Settings.General.Multiview.DrawSourceNames="显示场景名" +Basic.Settings.General.Multiview.DrawSafeAreas="显示安全区域(EBU R 95)" Basic.Settings.General.MultiviewLayout="多视图布局" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="水平、顶部" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="水平、底部" -Basic.Settings.General.MultiviewLayout.Vertical.Left="垂直, 左侧" -Basic.Settings.General.MultiviewLayout.Vertical.Right="垂直, 右侧" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="水平, 顶部(8 场景)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="水平, 底部(8 场景)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="垂直, 左侧(8 场景)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="垂直, 右侧(8 场景)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="水平, 顶部(24 场景)" Basic.Settings.Stream="流" Basic.Settings.Stream.StreamType="流类型" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="音频表衰减率" Basic.Settings.Audio.MeterDecayRate.Fast="快速" Basic.Settings.Audio.MeterDecayRate.Medium="中速(峰值电平表I型)" Basic.Settings.Audio.MeterDecayRate.Slow="慢速(峰值电平表II型)" +Basic.Settings.Audio.PeakMeterType="峰值计类型" +Basic.Settings.Audio.PeakMeterType.SamplePeak="采样峰值" +Basic.Settings.Audio.PeakMeterType.TruePeak="真峰值 (更高的的 CPU 使用率)" Basic.Settings.Audio.MultiChannelWarning.Enabled="警告: 已启用环绕声音频。" Basic.Settings.Audio.MultichannelWarning="如果串流, 请检查串流服务是否支持环绕立体声接收和环绕立体声播放。 Twitch, Facebook 360 Live, Mixer RTMP, Smashcast 是充分支持环绕立体声的例子。 虽然 Facebook Live 和 Youtube Live 都支持环绕立体声接收, 但是Facebook Live 降低混合至立体声, 而 Youtube Live 则只播放两个声道。\n\nOBS 音频过滤器与环绕立体声兼容, 但 VST 插件支持无法保证。" Basic.Settings.Audio.MultichannelWarning.Title="是否启用环绕立体声?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="网络" Basic.Settings.Advanced.Network.BindToIP="绑定 IP" Basic.Settings.Advanced.Network.EnableNewSocketLoop="启用新的网络代码" Basic.Settings.Advanced.Network.EnableLowLatencyMode="低延迟模式" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="当主窗口获得焦点时禁用热键" Basic.AdvAudio="高级音频属性" Basic.AdvAudio.Name="名称" @@ -682,10 +700,10 @@ Basic.AdvAudio.Volume="音量 (%)" Basic.AdvAudio.Mono="下降混合为单声道" Basic.AdvAudio.Panning="平移" Basic.AdvAudio.SyncOffset="同步偏移 (毫秒)" -Basic.AdvAudio.Monitoring="音频监测" +Basic.AdvAudio.Monitoring="音频监听" Basic.AdvAudio.Monitoring.None="关闭监视" Basic.AdvAudio.Monitoring.MonitorOnly="仅显示器(静音输出)" -Basic.AdvAudio.Monitoring.Both="监视器和输出" +Basic.AdvAudio.Monitoring.Both="监听并输出" Basic.AdvAudio.AudioTracks="轨道" Basic.Settings.Hotkeys="热键" @@ -749,3 +767,12 @@ OutputWarnings.MP4Recording="警告︰ 录制保存到 MP4 将无法恢复,如 FinalScene.Title="删除场景" FinalScene.Text="至少要有一个场景." +NoSources.Title="无来源" +NoSources.Text="看起来您未添加任何视频源,所以我们将只会输出黑色萤幕。确实要这样做吗?" +NoSources.Text.AddSource="您可以通过单击主窗口中“来源”框下的“+”图标以添加来源。" + +ChangeBG="设置颜色" +CustomColor="自定义颜色" + +BrowserSource.EnableHardwareAcceleration="启用浏览器源硬件加速" + diff --git a/UI/data/locale/zh-TW.ini b/UI/data/locale/zh-TW.ini index 8af7699..bc2a215 100644 --- a/UI/data/locale/zh-TW.ini +++ b/UI/data/locale/zh-TW.ini @@ -78,6 +78,8 @@ None="無" StudioMode.Preview="預覽" StudioMode.Program="程式" ShowInMultiview="在多視圖中顯示" +VerticalLayout="垂直排版" +Group="群組" AlreadyRunning.Title="OBS 已在執行中" AlreadyRunning.Text="OBS 已在執行中!除非這是您的意圖,請在執行新的 OBS 前關閉現存的 OBS 。如果有設定 OBS 最小化到系統工具列,請確認是否仍在該處執行。" @@ -405,6 +407,9 @@ Basic.Main.StoppingReplayBuffer="正在停止重播緩衝..." Basic.Main.StopStreaming="停止串流" Basic.Main.StoppingStreaming="停止串流..." Basic.Main.ForceStopStreaming="停止實況(丟棄延遲)" +Basic.Main.Group="群組 %1" +Basic.Main.GroupItems="群組選取的項目" +Basic.Main.Ungroup="取消群組" Basic.MainMenu.File="檔案 (&F)" Basic.MainMenu.File.Export="匯出 (&E)" @@ -471,12 +476,16 @@ Basic.MainMenu.Tools="工具(&T)" Basic.MainMenu.Help="說明 (&H)" Basic.MainMenu.Help.HelpPortal="幫助和門戶" Basic.MainMenu.Help.Website="前往 OBS 網站 (&W)" +Basic.MainMenu.Help.Discord="加入 &Discord 伺服器" Basic.MainMenu.Help.Logs="Log 檔案 (&L)" Basic.MainMenu.Help.Logs.ShowLogs="顯示 Log (&S)" Basic.MainMenu.Help.Logs.UploadCurrentLog="上傳目前 Log 檔 (&C)" Basic.MainMenu.Help.Logs.UploadLastLog="上傳上次的 Log 檔 (&L)" Basic.MainMenu.Help.Logs.ViewCurrentLog="顯示當前紀錄檔 (&V)" Basic.MainMenu.Help.CheckForUpdates="檢查更新" +Basic.MainMenu.Help.CrashLogs="錯誤報告" +Basic.MainMenu.Help.CrashLogs.ShowLogs="顯示當機回報(&S)" +Basic.MainMenu.Help.CrashLogs.UploadLastLog="上傳最新的錯誤回報(&L)" Basic.Settings.ProgramRestart="為了套用新設定,請關閉後重啟。" Basic.Settings.ConfirmTitle="確認修改" @@ -507,11 +516,16 @@ Basic.Settings.General.SystemTrayHideMinimize="總是最小化到系統列,而 Basic.Settings.General.SaveProjectors="退出時保存投影設定" Basic.Settings.General.SwitchOnDoubleClick="按兩下時切換到場景" Basic.Settings.General.StudioPortraitLayout="啟用縱向/垂直佈局" +Basic.Settings.General.Multiview="多顯示" +Basic.Settings.General.Multiview.MouseSwitch="點擊以在場景之間切換" +Basic.Settings.General.Multiview.DrawSourceNames="顯示場景名稱" +Basic.Settings.General.Multiview.DrawSafeAreas="繪製安全區域 (EBU R 95)" Basic.Settings.General.MultiviewLayout="多視圖佈局" -Basic.Settings.General.MultiviewLayout.Horizontal.Top="水平, 上" -Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="水平, 下" -Basic.Settings.General.MultiviewLayout.Vertical.Left="垂直, 左" -Basic.Settings.General.MultiviewLayout.Vertical.Right="垂直, 右" +Basic.Settings.General.MultiviewLayout.Horizontal.Top="橫排、頂部(八個場景)" +Basic.Settings.General.MultiviewLayout.Horizontal.Bottom="橫排、底部(八個場景)" +Basic.Settings.General.MultiviewLayout.Vertical.Left="直行、靠左(八個場景)" +Basic.Settings.General.MultiviewLayout.Vertical.Right="直行、靠右(八個場景)" +Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="橫排、頂部(24 個場景)" Basic.Settings.Stream="串流" Basic.Settings.Stream.StreamType="串流類型" @@ -635,6 +649,9 @@ Basic.Settings.Audio.MeterDecayRate="音量計衰減速率" Basic.Settings.Audio.MeterDecayRate.Fast="快速" Basic.Settings.Audio.MeterDecayRate.Medium="中 (Type I PPM)" Basic.Settings.Audio.MeterDecayRate.Slow="慢 (Type II PPM)" +Basic.Settings.Audio.PeakMeterType="峰值表類型" +Basic.Settings.Audio.PeakMeterType.SamplePeak="範例峰值" +Basic.Settings.Audio.PeakMeterType.TruePeak="真實峰值(較高的 CPU 用量)" Basic.Settings.Audio.MultiChannelWarning.Enabled="警告: 已啟用環繞聲音訊。" Basic.Settings.Audio.MultichannelWarning="如果是流媒體,請檢查您的流媒體服務是否同時支持環繞聲攝取和環繞聲播放。 Twitch,Facebook 360 Live,Mixer RTMP,Smashcast都是完全支持環繞聲的例子。 儘管Facebook Live和YouTube Live都接受環繞聲攝取,但是Facebook Live會混音為立體聲,而YouTube Live只播放兩個聲道。\n\n 儘管不支持VST插件,但OBS音頻濾波器與環繞聲兼容。" Basic.Settings.Audio.MultichannelWarning.Title="是否啟用環繞聲音訊?" @@ -675,6 +692,7 @@ Basic.Settings.Advanced.Network="網路" Basic.Settings.Advanced.Network.BindToIP="綁定到 IP" Basic.Settings.Advanced.Network.EnableNewSocketLoop="啟用新的網路程式碼" Basic.Settings.Advanced.Network.EnableLowLatencyMode="低延遲模式" +Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus="當主視窗處於焦點,則停用熱鍵" Basic.AdvAudio="進階音訊屬性" Basic.AdvAudio.Name="名稱" @@ -749,3 +767,11 @@ OutputWarnings.MP4Recording="警告︰ 如果檔案無法完成,儲存成 MP4 FinalScene.Title="刪除場景" FinalScene.Text="至少要有一個場景。" +NoSources.Title="沒有來源" +NoSources.Text="看起來您尚未增加任何視訊來源,所以我們只會輸出黑畫面。確定?" +NoSources.Text.AddSource="您可以透過點一下主視窗「來源」框底下的 + 按鈕,隨時增加來源。" + +ChangeBG="設定顏色" +CustomColor="自訂顏色" + + diff --git a/UI/data/themes/Acri.qss b/UI/data/themes/Acri.qss index af3eb90..ca9637e 100644 --- a/UI/data/themes/Acri.qss +++ b/UI/data/themes/Acri.qss @@ -1,3 +1,37 @@ +/* OBSTheme, main QApplication palette and QML values */ +OBSTheme { + window: #181819; + windowText: rgb(225,224,225); + base: rgb(18,18,21); + alternateBase: rgb(0,0,0); + text: rgb(225,224,225); + button: #162458; + buttonText: rgb(225,224,225); + brightText: #484848; + + light: #162458; + mid: #181819; + dark: rgb(18,18,21); + shadow: rgb(0,0,0); + + highlight: #252458; + highlightText: #FFFFFF; + + link: #605ee6; + linkVisited: #605ee6; +} + +OBSTheme::disabled { + text: #484848; + buttonText: #484848; + brightText: #484848; +} + +OBSTheme::inactive { + highlight: rgb(48,47,48); + highlightText: rgb(255, 255, 255); +} + /* General style, we override only what is needed. */ QWidget { background-color: #181819; @@ -9,6 +43,7 @@ QWidget { font-family: "Open Sans", "Tahoma", "Arial", sans-serif; font-size: 12px; padding: 4px; + overflow: auto; } #menubar { @@ -45,6 +80,14 @@ QWidget::disabled { color: #484848; } +* [themeID="error"] { + color: #d91740; +} + +* [themeID="warning"] { + color: #d9af17; +} + /* Dropdown menus, Scenes box, Sources box */ QAbstractItemView { background-color: #181819; @@ -94,7 +137,8 @@ QMenuBar::item:selected { } /* Listbox item */ -QListWidget::item { +QListWidget::item, +SourceTree::item { padding: 4px 2px; margin-bottom: 2px; margin-top: 0px; @@ -110,11 +154,6 @@ QListWidget QLineEdit { border-radius: none; } -SourceListWidget::item { - margin-bottom: 1px; - padding: -4px 2px; -} - /* Dock stuff */ QDockWidget { background: transparent; @@ -157,6 +196,23 @@ SourceListWidget { border-bottom: 2px solid #2f2f2f; } +SourceTree { + border: none; + border-bottom: 1px solid #2f2f2f; +} + +SourceTree QLabel { + padding: 2px 0px; + margin: -2px 4px -2px; +} + +SourceTree QLineEdit { + background-color: #0c101e; + padding: 2px; + margin: -2px 6px -2px 3px; + font-size: 12px; +} + #scenesFrame, #sourcesFrame { margin-left: -7px; @@ -179,13 +235,15 @@ SourceListWidget { } /* Listbox item selected, unfocused */ -QListWidget::item:hover { +QListWidget::item:hover, +SourceTree::item:hover { background-color: #212121; border: 1px solid #333336; } /* Listbox item selected */ -QListWidget::item:selected { +QListWidget::item:selected, +SourceTree::item:selected { background-color: #131a30; border: 1px solid #252a45; } @@ -453,10 +511,12 @@ QPushButton { QPushButton::flat { background-color: rgb(24,24,25); + border: none; } QPushButton:checked { - background-color: #202b52; + background-color: #581624; + border-color: #84162d; } QPushButton:hover { @@ -468,6 +528,16 @@ QPushButton:pressed { background-color: #161f41; } +QPushButton:disabled { + border: 1px solid #232426; + background-color: #1a1a1b; +} + +QPushButton::flat:hover, +QPushButton::flat:disabled { + border: none; +} + /* Progress Bar */ QProgressBar { @@ -505,16 +575,20 @@ QSlider::handle:horizontal { } QSlider::handle:horizontal:pressed { - background-color: QLinearGradient(x1: 0, y1: 1, x2: 0, y2: 0, + background-color: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgb(240,239,240), stop: 0.25 rgb(200,199,200), stop: 1 rgb(162,161,162)); } +QSlider::sub-page:horizontal { + background-color: #2a3a75; +} + QSlider::sub-page:horizontal:disabled { - background-color: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 rgb(31,30,31), - stop: 0.75 rgb(50, 49, 50)); + background-color: QLinearGradient(x1: 0, y1: 1, x2: 0, y2: 0, + stop: 0 rgb(26,25,26), + stop: 0.75 rgb(10, 10, 10)); border-radius: 2px; } @@ -533,23 +607,27 @@ QSlider::handle:vertical { stop: 0.25 rgb(200,199,200), stop: 1 rgb(162,161,162)); border: 1px solid rgb(24,24,25); - border-radius: 4px; + border-radius: 3px; width: 10px; height: 18px; - margin: -3px 0; /* handle is placed by default on the contents rect of the groove. Expand outside the groove */ + margin: 0 -3px; /* handle is placed by default on the contents rect of the groove. Expand outside the groove */ } QSlider::handle:vertical:pressed { - background-color: QLinearGradient(x1: 1, y1: 0, x2: 0, y2: 0, + background-color: QLinearGradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 rgb(240,239,240), stop: 0.25 rgb(200,199,200), stop: 1 rgb(162,161,162)); } -QSlider::sub-page:vertical:disabled { +QSlider::add-page:vertical { + background-color: #2a3a75; +} + +QSlider::add-page:vertical:disabled { background-color: QLinearGradient(x1: 0, y1: 0, x2: 1, y2: 0, - stop: 0 rgb(31,30,31), - stop: 0.75 rgb(50, 49, 50)); + stop: 0 rgb(26,25,26), + stop: 0.75 rgb(10, 10, 10)); border-radius: 2px; } @@ -557,15 +635,10 @@ QSlider::handle:hover { background-color: rgb(200,199,200); } -QSlider::sub-page { - background-color: #2a3a75; -} - QSlider::handle:disabled { background-color: rgb(15,15,16); } - /* Volume Control */ /* Old Meters */ @@ -724,6 +797,30 @@ OBSHotkeyLabel[hotkeyPairHover=true] { } +/* Group Collapse Checkbox */ + +SourceTreeSubItemCheckBox { + background: transparent; + outline: none; + padding: 0px; +} + +SourceTreeSubItemCheckBox::indicator { + width: 12px; + height: 12px; +} + +SourceTreeSubItemCheckBox::indicator:checked, +SourceTreeSubItemCheckBox::indicator:checked:hover { + image: url(./Dark/expand.png); +} + +SourceTreeSubItemCheckBox::indicator:unchecked, +SourceTreeSubItemCheckBox::indicator:unchecked:hover { + image: url(./Dark/collapse.png); +} + + /* Label warning/error */ QLabel#warningLabel { @@ -750,21 +847,10 @@ OBSBasicProperties, background: #101010; } -#OBSBasicSourceSelect #sourceList { - border-bottom: 2px solid #333336; -} - FocusList::item { padding: 0px 2px; } -#transitionsContainer QPushButton, -#mixerDock QPushButton, -#effectWidget QPushButton, -#asyncWidget QPushButton { - border: none; -} - #fpsTypes { padding: 0px; } diff --git a/UI/data/themes/Dark.qss b/UI/data/themes/Dark.qss index 4a9326c..934706a 100644 --- a/UI/data/themes/Dark.qss +++ b/UI/data/themes/Dark.qss @@ -28,6 +28,44 @@ /* rgb(42,130,218); /* blue */ +/* Custom theme information. This will set the application's QPalette, as + * well as pass to QML via the OBSTheme object. + * Can also use OBSTheme::disabled, OBSTheme::active, and OBSTheme::inactive. + * Using it without will set all three (making 'active' a bit redundant) */ +OBSTheme { + window: rgb(58,57,58); /* dark */ + windowText: rgb(225,224,225); /* veryLight */ + base: rgb(31,30,31); /* veryDark */ + alternateBase: rgb(11,10,11); /* veryVeryDark */ + text: rgb(225,224,225); /* veryLight */ + button: rgb(88,87,88); /* kindaDark */ + buttonText: rgb(225,224,225); /* veryLight */ + brightText: rgb(200,199,200); /* lighter */ + + light: rgb(88,87,88); /* kindaDark */ + mid: rgb(58,57,58); /* dark */ + dark: rgb(31,30,31); /* veryDark */ + shadow: rgb(11,10,11); /* veryVeryDark */ + + highlight: rgb(42,130,218); /* blue */ + highlightText: rgb(0,0,0); + + link: rgb(114, 162, 255); /* OBS blue */ + linkVisited: rgb(114, 162, 255); /* OBS blue */ +} + +OBSTheme::disabled { + text: rgb(122,121,122); /* light */ + buttonText: rgb(122,121,122); /* light */ + brightText: rgb(122,121,122); /* light */ +} + +OBSTheme::inactive { + highlight: rgb(48,47,48); + highlightText: rgb(255, 255, 255); +} + + /* General style, we override only what is needed. */ QWidget { background-color: rgb(58,57,58); /* dark */ @@ -423,9 +461,9 @@ QPushButton::menu-indicator { /* Sliders */ QSlider::groove:horizontal { - background-color: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 rgb(31,30,31), /* veryDark */ - stop: 0.75 rgb(50, 49, 50)); + background-color: QLinearGradient(x1: 0, y1: 1, x2: 0, y2: 0, + stop: 0 rgb(50, 49, 50), /* dark */ + stop: 0.75 rgb(88,87,88)); /* kindaDark */ height: 4px; border: none; border-radius: 2px; @@ -450,32 +488,37 @@ QSlider::handle:horizontal:pressed { stop: 1 rgb(162,161,162)); /* light */ } +QSlider::sub-page:horizontal { + background-color: rgb(42,130,218); /* blue */ + border-radius: 2px; +} + QSlider::sub-page:horizontal:disabled { - background-color: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1, + background-color: QLinearGradient(x1: 0, y1: 1, x2: 0, y2: 0, stop: 0 rgb(31,30,31), /* veryDark */ - stop: 0.75 rgb(50, 49, 50)); + stop: 0.75 rgb(50, 49, 50)); /* dark */ border-radius: 2px; } QSlider::groove:vertical { - background-color: QLinearGradient(x1: 0, y1: 0, x2: 1, y2: 0, - stop: 0 rgb(31,30,31), /* veryDark */ - stop: 0.75 rgb(50, 49, 50)); + background-color: QLinearGradient(x1: 1, y1: 0, x2: 0, y2: 0, + stop: 0 rgb(50, 49, 50), /* dark */ + stop: 0.75 rgb(88,87,88)); /* kindaDark */ width: 4px; border: none; border-radius: 2px; } QSlider::handle:vertical { - background-color: QLinearGradient(x1: 0, y1: 0, x2: 1, y2: 0, + background-color: QLinearGradient(x1: 1, y1: 0, x2: 0, y2: 0, stop: 0 rgb(240,239,240), /* lighter */ stop: 0.25 rgb(200,199,200), stop: 1 rgb(162,161,162)); /* light */ border: 1px solid rgb(58,57,58); /* dark */ - border-radius: 4px; + border-radius: 3px; width: 10px; height: 18px; - margin: -3px 0; /* handle is placed by default on the contents rect of the groove. Expand outside the groove */ + margin: 0 -3px; /* handle is placed by default on the contents rect of the groove. Expand outside the groove */ } QSlider::handle:vertical:pressed { @@ -485,10 +528,15 @@ QSlider::handle:vertical:pressed { stop: 1 rgb(162,161,162)); /* light */ } -QSlider::sub-page:vertical:disabled { - background-color: QLinearGradient(x1: 0, y1: 0, x2: 1, y2: 0, +QSlider::add-page:vertical { + background-color: rgb(42,130,218); /* blue */ + border-radius: 2px; +} + +QSlider::add-page:vertical:disabled { + background-color: QLinearGradient(x1: 1, y1: 0, x2: 0, y2: 0, stop: 0 rgb(31,30,31), /* veryDark */ - stop: 0.75 rgb(50, 49, 50)); + stop: 0.75 rgb(50, 49, 50)); /* dark */ border-radius: 2px; } @@ -496,16 +544,10 @@ QSlider::handle:hover { background-color: rgb(200,199,200); /* veryLight */ } -QSlider::sub-page { - background-color: rgb(42,130,218); /* blue */ - border-radius: 2px; -} - QSlider::handle:disabled { background-color: rgb(122,121,122); /* light */ } - /* Volume Control */ VolumeMeter { @@ -547,6 +589,27 @@ OBSHotkeyLabel[hotkeyPairHover=true] { } +/* Group Collapse Checkbox */ + +SourceTreeSubItemCheckBox { + background: transparent; + outline: none; +} + +SourceTreeSubItemCheckBox::indicator { + width: 10px; + height: 10px; +} + +SourceTreeSubItemCheckBox::indicator:checked { + image: url(./Dark/expand.png); +} + +SourceTreeSubItemCheckBox::indicator:unchecked { + image: url(./Dark/collapse.png); +} + + /* Label warning/error */ QLabel#warningLabel { diff --git a/UI/data/themes/Dark/collapse.png b/UI/data/themes/Dark/collapse.png new file mode 100644 index 0000000..1fd72a1 Binary files /dev/null and b/UI/data/themes/Dark/collapse.png differ diff --git a/UI/data/themes/Dark/expand.png b/UI/data/themes/Dark/expand.png new file mode 100644 index 0000000..dd5d2b5 Binary files /dev/null and b/UI/data/themes/Dark/expand.png differ diff --git a/UI/data/themes/Default.qss b/UI/data/themes/Default.qss index 0b88dd4..37f4b20 100644 --- a/UI/data/themes/Default.qss +++ b/UI/data/themes/Default.qss @@ -51,6 +51,24 @@ MuteCheckBox::indicator:unchecked { image: url(:/res/images/unmute.png); } +SourceTreeSubItemCheckBox { + background: transparent; + outline: none; +} + +SourceTreeSubItemCheckBox::indicator { + width: 10px; + height: 10px; +} + +SourceTreeSubItemCheckBox::indicator:checked { + image: url(:/res/images/expand.png); +} + +SourceTreeSubItemCheckBox::indicator:unchecked { + image: url(:/res/images/collapse.png); +} + OBSHotkeyLabel[hotkeyPairHover=true] { color: red; } diff --git a/UI/data/themes/Rachni.qss b/UI/data/themes/Rachni.qss index 5c7887c..e5a8362 100644 --- a/UI/data/themes/Rachni.qss +++ b/UI/data/themes/Rachni.qss @@ -40,6 +40,76 @@ /***************************************************************************/ +/************************/ +/* ---- Main Theme ---- */ +/************************/ + +OBSTheme { + window: rgb(49, 54, 59); /* Blue-gray */ + windowText: rgb(239, 240, 241); /* White */ + base: rgb(0, 139, 163); /* Dark Cyan (Primary Dark) */ + alternateBase: rgb(186, 45, 101); /* Dark Pink (Secondary Dark) */ + text: rgb(239, 240, 241); /* White */ + button: rgb(0, 188, 212); /* Cyan (Primary) */ + buttonText: rgb(239, 240, 241); /* White */ + brightText: rgb(255, 148, 194); /* Light Pink (Secondary Light) */ + + light: rgb(162, 161, 162); /* Lighter Gray */ + mid: rgb(118, 121, 124); /* Light Grey */ + dark: rgb(84, 87, 91); /* Gray */ + shadow: rgb(35, 38, 41); /* Dark Gray */ + + highlight: rgb(98, 238, 255); /* Light Cyan (Primary Light) */ + highlightText: rgb(0,0,0); + + link: rgb(98, 238, 255); /* Light Cyan (Primary Light) */ + linkVisited: rgb(98, 238, 255); /* Light Cyan (Primary Light) */ +} + +OBSTheme::disabled { + text: rgb(118, 121, 124); /* Light Gray */ + buttonText: rgb(118, 121, 124); /* Light Gray */ + brightText: rgb(118, 121, 124); /* Light Gray */ +} + +OBSTheme::inactive { + highlight: rgb(0, 188, 212); /* Cyan (Primary) */ + highlightText: rgb(239, 240, 241); /* White */ +} + + +/************************/ +/* ---- SourceTree ---- */ +/************************/ + +SourceTree::item:selected:!active { + color: rgb(239, 240, 241); /* White */ + background-color: rgba(255, 148, 194, 0.25); /* Light Pink (Secondary Light) */ + border: none; +} + +SourceTree::item:selected { + background-color: rgba(240, 98, 146, 0.5); /* Pink (Secondary) */ + border: none; +} + +SourceTree::item:hover, +SourceTree::item:disabled:hover, +SourceTree::item:hover:!active { + background-color: rgb(0, 188, 212); /* Cyan (Primary) */ + color: rgb(239, 240, 241); /* White */ + border: none; +} + +SourceTree QLineEdit { + padding-top: 0; + padding-bottom: 0; + padding-right: 0; + padding-left: 2px; + border: none; + border-radius: none; +} + /*************************/ /* --- General style --- */ /*************************/ @@ -535,16 +605,6 @@ QLineEdit { color: rgb(239, 240, 241); /* White */ } -QListWidget QLineEdit { - padding-top: 0; - padding-bottom: 0; - padding-right: 0; - padding-left: 2px; - border: none; - border-radius: none; -} - - /**********************/ /* --- Checkboxes --- */ /**********************/ @@ -701,6 +761,30 @@ MuteCheckBox::indicator:unchecked:disabled { image: url(./Dark/unmute.png); } +/****************************/ +/* --- Group Checkboxes --- */ +/****************************/ + +SourceTreeSubItemCheckBox { + background: transparent; + outline: none; +} + +SourceTreeSubItemCheckBox::indicator { + width: 10px; + height: 10px; +} + +SourceTreeSubItemCheckBox::indicator:checked, +SourceTreeSubItemCheckBox::indicator:checked:hover { + image: url(./Dark/expand.png); +} + +SourceTreeSubItemCheckBox::indicator:unchecked, +SourceTreeSubItemCheckBox::indicator:unchecked:hover { + image: url(./Dark/collapse.png); +} + /*************************/ /* --- Progress bars --- */ /*************************/ @@ -775,10 +859,10 @@ QPushButton:disabled { } QPushButton::menu-indicator { - image: url(./Rachni/down_arrow.png); - subcontrol-position: right; - subcontrol-origin: padding; - width: 25px; + image: url(./Rachni/down_arrow.png); + subcontrol-position: right; + subcontrol-origin: padding; + width: 25px; } /******************************/ @@ -1022,10 +1106,55 @@ QSlider::handle:horizontal:pressed { stop: 1 rgb(162, 161, 162)); } +QSlider::sub-page:horizontal { + background-color: rgb(0, 188, 212); /* Cyan (Primary) */ + border-radius: 2px; +} + QSlider::sub-page:horizontal:disabled { background-color: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgb(35, 38, 41), /* Dark Gray */ + stop: 0.75 rgb(35, 38, 41)); /* Dark Gray */ + border-radius: 2px; +} + +QSlider::groove:vertical { + background-color: QLinearGradient(x1: 1, y1: 0, x2: 0, y2: 0, + stop: 0 rgb(35, 38, 41), /* Dark Gray */ stop: 0.75 rgb(50, 49, 50)); + width: 4px; + border: none; + border-radius: 2px; +} + +QSlider::handle:vertical { + background-color: QLinearGradient(x1: 1, y1: 0, x2: 0, y2: 0, + stop: 0 rgb(240, 239, 240), + stop: 0.25 rgb(200, 199, 200), + stop: 1 rgb(162, 161, 162)); + border: 1px solid rgb(58, 57, 58); + border-radius: 3px; + width: 10px; + height: 18px; + margin: 0 -3px; +} + +QSlider::handle:vertical:pressed { + background-color: QLinearGradient(x1: 1, y1: 0, x2: 0, y2: 0, + stop: 0 rgb(240, 239, 240), + stop: 0.25 rgb(200, 199, 200), + stop: 1 rgb(162, 161, 162)); +} + +QSlider::add-page:vertical { + background-color: rgb(0, 188, 212); /* Cyan (Primary) */ + border-radius: 2px; +} + +QSlider::add-page:vertical:disabled { + background-color: QLinearGradient(x1: 1, y1: 0, x2: 0, y2: 0, + stop: 0 rgb(35, 38, 41), /* Dark Gray */ + stop: 0.75 rgb(35, 38, 41)); /* Dark Gray */ border-radius: 2px; } @@ -1033,11 +1162,6 @@ QSlider::handle:hover { background-color: rgb(200, 199, 200); } -QSlider::sub-page { - background-color: rgb(0, 188, 212); /* Cyan (Primary) */ - border-radius: 2px; -} - QSlider::handle:disabled { background-color: rgb(122, 121, 122); } @@ -1069,6 +1193,14 @@ QFrame[frameShape="0"] { /* Misc style tweaks for dark themes */ +* [themeID="error"] { + color: rgb(255, 89, 76); /* Red Error */ +} + +* [themeID="warning"] { + color: rgb(255, 148, 194); /* Light Pink (Secondary Light) */ +} + QStatusBar::item { border: none; } diff --git a/UI/expand-checkbox.hpp b/UI/expand-checkbox.hpp new file mode 100644 index 0000000..375a4ce --- /dev/null +++ b/UI/expand-checkbox.hpp @@ -0,0 +1,5 @@ +#include + +class ExpandCheckBox : public QCheckBox { + Q_OBJECT +}; diff --git a/UI/forms/ColorSelect.ui b/UI/forms/ColorSelect.ui new file mode 100644 index 0000000..b07e1f9 --- /dev/null +++ b/UI/forms/ColorSelect.ui @@ -0,0 +1,265 @@ + + + ColorSelect + + + + 0 + 0 + 98 + 61 + + + + + 0 + 0 + + + + Form + + + QPushButton { + border: 1px solid black; +} + + + + + + + + + 0 + 0 + + + + + 16 + 16 + + + + + 16 + 16 + + + + + + + + + + 1 + + + + + + + + 0 + 0 + + + + + 16 + 16 + + + + + 16 + 16 + + + + + + + 2 + + + + + + + + 0 + 0 + + + + + 16 + 16 + + + + + 16 + 16 + + + + + + + 3 + + + + + + + + 0 + 0 + + + + + 16 + 16 + + + + + 16 + 16 + + + + + + + 4 + + + + + + + + 0 + 0 + + + + + 16 + 16 + + + + + 16 + 16 + + + + + + + 5 + + + + + + + + 0 + 0 + + + + + 16 + 16 + + + + + 16 + 16 + + + + + + + 6 + + + + + + + + 0 + 0 + + + + + 16 + 16 + + + + + 16 + 16 + + + + + + + 7 + + + + + + + + 0 + 0 + + + + + 16 + 16 + + + + + 16 + 16 + + + + + + + + + + + + + + + + + + + diff --git a/UI/forms/OBSBasic.ui b/UI/forms/OBSBasic.ui index 55e66c4..983a869 100644 --- a/UI/forms/OBSBasic.ui +++ b/UI/forms/OBSBasic.ui @@ -146,6 +146,7 @@ + @@ -518,7 +519,7 @@ 0 - + 0 @@ -615,63 +616,112 @@ 4 - - - - 220 - 0 - - - - Qt::CustomContextMenu - - - QFrame::StyledPanel - - - QFrame::Sunken - - - Qt::ScrollBarAlwaysOn - - - Qt::ScrollBarAlwaysOff - - - true - - - - - 0 - 0 - 230 - 16 - + + + + Qt::CustomContextMenu - - - 0 - 0 - + + QFrame::StyledPanel - - - 0 + + QFrame::Sunken + + + Qt::ScrollBarAlwaysOn + + + Qt::ScrollBarAlwaysOff + + + true + + + + + 0 + 0 + 230 + 16 + - - 0 + + + 0 + 0 + - - 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Qt::CustomContextMenu + + + QFrame::StyledPanel + + + QFrame::Sunken + + + Qt::ScrollBarAlwaysOff + + + Qt::ScrollBarAlwaysOn + + + true + + + + + 0 + 0 + 16 + 230 + - - 0 + + + 0 + 0 + - - 0 - - + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + @@ -1507,7 +1557,7 @@ - Basic.AutoConfig.Beta + Basic.AutoConfig @@ -1601,6 +1651,11 @@ Basic.MainMenu.Help.CrashLogs.UploadLastLog + + + Basic.MainMenu.Help.Discord + + @@ -1614,6 +1669,12 @@ QStatusBar
window-basic-status-bar.hpp
+ + HScrollArea + QScrollArea +
horizontal-scroll-area.hpp
+ 1 +
VScrollArea QScrollArea @@ -1621,9 +1682,9 @@ 1 - SourceListWidget - QListWidget -
source-list-widget.hpp
+ SourceTree + QListView +
source-tree.hpp
diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui index c89cc1e..c92fc9e 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -146,7 +146,7 @@ 0 0 801 - 741 + 836 @@ -582,10 +582,74 @@
+ + + + + + + Basic.Settings.General.Multiview + + + + QFormLayout::AllNonFixedFieldsGrow + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 2 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 170 + 5 + + + + + + + + Basic.Settings.General.Multiview.MouseSwitch + + + true + + + + + + + Basic.Settings.General.Multiview.DrawSourceNames + + + true + + + + + + Basic.Settings.General.Multiview.DrawSafeAreas + + + true + + + + - + Basic.Settings.General.MultiviewLayout @@ -3362,49 +3426,6 @@ - - - - true - - - - - 0 - 0 - 800 - 69 - - - - - - - - - color: rgb(255, 0, 4); - - - - - - true - - - - - - - - - - true - - - warning - - - @@ -3440,6 +3461,86 @@ + + + + Basic.Settings.Audio.PeakMeterType + + + + + + + 0 + + + + Basic.Settings.Audio.PeakMeterType.SamplePeak + + + + + Basic.Settings.Audio.PeakMeterType.TruePeak + + + + + + + + true + + + + + 0 + 0 + 800 + 69 + + + + + + + + + + + + true + + + error + + + + + + + + + + true + + + warning + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + @@ -3744,8 +3845,8 @@ 0 0 - 98 - 28 + 818 + 697 @@ -3762,7 +3863,7 @@ - + 0 @@ -3791,11 +3892,11 @@ 0 0 - 593 - 761 + 803 + 807 - + 0 @@ -3808,9 +3909,9 @@ 9 - + - + @@ -3820,6 +3921,12 @@ QFormLayout::AllNonFixedFieldsGrow + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 2 + @@ -3833,6 +3940,19 @@ + + + + Qt::Horizontal + + + + 170 + 0 + + + + @@ -3848,6 +3968,9 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + 2 + @@ -4020,6 +4143,19 @@ + + + + Qt::Horizontal + + + + 170 + 20 + + + + @@ -4032,6 +4168,12 @@ QFormLayout::AllNonFixedFieldsGrow + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 2 + @@ -4052,6 +4194,19 @@ + + + + Qt::Horizontal + + + + 170 + 20 + + + + @@ -4067,6 +4222,9 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + 2 + @@ -4129,6 +4287,19 @@ + + + + Qt::Horizontal + + + + 170 + 20 + + + + @@ -4141,6 +4312,9 @@ QFormLayout::AllNonFixedFieldsGrow + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + 2 @@ -4230,6 +4404,19 @@ + + + + Qt::Horizontal + + + + 170 + 20 + + + + @@ -4242,6 +4429,12 @@ QFormLayout::AllNonFixedFieldsGrow + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 2 + @@ -4311,6 +4504,19 @@ + + + + Qt::Horizontal + + + + 170 + 20 + + + + @@ -4323,9 +4529,12 @@ QFormLayout::AllNonFixedFieldsGrow - - - + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 2 + @@ -4336,6 +4545,9 @@ + + + @@ -4353,33 +4565,84 @@ + + + + Qt::Horizontal + + + + 170 + 20 + + + + - - - color: rgb(255, 0, 4); - - - - - - true + + + Basic.Main.Sources + + + 2 + + + + + Qt::Horizontal + + + + 170 + 20 + + + + + + + + BrowserSource.EnableHardwareAcceleration + + + + - - - color: rgb(255, 0, 4); - - - - - - true + + + Basic.Settings.Hotkeys + + + 2 + + + + + Basic.Settings.Advanced.Hotkeys.DisableHotkeysInFocus + + + + + + + Qt::Horizontal + + + + 170 + 20 + + + + + @@ -4389,6 +4652,48 @@ + + + + 10 + + + 10 + + + 10 + + + 10 + + + + + color: rgb(255, 0, 4); + + + + + + true + + + + + + + color: rgb(255, 0, 4); + + + + + + true + + + + + diff --git a/UI/forms/images/collapse.png b/UI/forms/images/collapse.png new file mode 100644 index 0000000..04707a4 Binary files /dev/null and b/UI/forms/images/collapse.png differ diff --git a/UI/forms/images/expand.png b/UI/forms/images/expand.png new file mode 100644 index 0000000..1222bcd Binary files /dev/null and b/UI/forms/images/expand.png differ diff --git a/UI/forms/obs.qrc b/UI/forms/obs.qrc index 367fc13..a68bc70 100644 --- a/UI/forms/obs.qrc +++ b/UI/forms/obs.qrc @@ -17,6 +17,8 @@ images/tray_active.png images/locked_mask.png images/unlocked_mask.png + images/collapse.png + images/expand.png images/settings/advanced.png diff --git a/UI/frontend-plugins/frontend-tools/CMakeLists.txt b/UI/frontend-plugins/frontend-tools/CMakeLists.txt index 37a022a..ca29cad 100644 --- a/UI/frontend-plugins/frontend-tools/CMakeLists.txt +++ b/UI/frontend-plugins/frontend-tools/CMakeLists.txt @@ -25,6 +25,7 @@ set(frontend-tools_HEADERS tool-helpers.hpp ../../properties-view.hpp ../../properties-view.moc.hpp + ../../horizontal-scroll-area.hpp ../../vertical-scroll-area.hpp ../../double-slider.hpp ) @@ -34,6 +35,7 @@ set(frontend-tools_SOURCES frontend-tools.c output-timer.cpp ../../properties-view.cpp + ../../horizontal-scroll-area.cpp ../../vertical-scroll-area.cpp ../../double-slider.cpp ) diff --git a/UI/frontend-plugins/frontend-tools/captions.cpp b/UI/frontend-plugins/frontend-tools/captions.cpp index 43ebf94..5c68d87 100644 --- a/UI/frontend-plugins/frontend-tools/captions.cpp +++ b/UI/frontend-plugins/frontend-tools/captions.cpp @@ -1,4 +1,5 @@ #include +#include #include #include diff --git a/UI/frontend-plugins/frontend-tools/data/locale/el-GR.ini b/UI/frontend-plugins/frontend-tools/data/locale/el-GR.ini index 7684795..0fa26e4 100644 --- a/UI/frontend-plugins/frontend-tools/data/locale/el-GR.ini +++ b/UI/frontend-plugins/frontend-tools/data/locale/el-GR.ini @@ -25,5 +25,18 @@ OutputTimer.Record.StoppingIn="Διακοπή εγγραφής σε:" OutputTimer.Stream.EnableEverytime="Ενεργοποίηση χρονόμετρου streaming κάθε φορά" OutputTimer.Record.EnableEverytime="Ενεργοποίηση χρονόμετρου εγγραφής κάθε φορά" +Scripts="Δέσμες ενεργειών" +LoadedScripts="Φορτωμένες δέσμες ενεργειών" +AddScripts="Προσθήκη δεσμών ενεργειών" +RemoveScripts="Αφαίρεση δεσμών ενεργειών" +ReloadScripts="Επαναφόρτωση δεσμών ενεργειών" +PythonSettings="Ρυθμίσεις Python" +PythonSettings.PythonInstallPath32bit="Χώρος εγκατάστασης Python (32bit)" +PythonSettings.PythonInstallPath64bit="Χώρος εγκατάστασης Python (64bit)" +PythonSettings.BrowsePythonPath="Περιήγηση τού χώρου εγκατάστασης Python" +ScriptLogWindow="Script Log" +Description="Περιγραφή" +FileFilter.ScriptFiles="Κρυπτογραφημένο αρχείο δέσμης ενεργειών" +FileFilter.AllFiles="Όλα τα αρχεία" diff --git a/UI/frontend-plugins/frontend-tools/data/locale/gd-GB.ini b/UI/frontend-plugins/frontend-tools/data/locale/gd-GB.ini new file mode 100644 index 0000000..c7597a6 --- /dev/null +++ b/UI/frontend-plugins/frontend-tools/data/locale/gd-GB.ini @@ -0,0 +1,42 @@ +SceneSwitcher="Suidsear fèin-obrachail nan sealladh" +SceneSwitcher.OnNoMatch="Mura freagair uinneag" +SceneSwitcher.OnNoMatch.DontSwitch="Na dèan suids" +SceneSwitcher.OnNoMatch.SwitchTo="Dèan suids gu:" +SceneSwitcher.CheckInterval="Thoir sùil air tiotal na h-uinneige gnìomhaich gach:" +SceneSwitcher.ActiveOrNotActive="Tha suidsear nan sealladh:" +InvalidRegex.Title="Chan eil an t-eas-preisean riaghailteach dligheach" +InvalidRegex.Text="Chan eil an t-eas-preisean riaghailteach a chuir thu a-steach dligheach." +Active="Gnìomhach" +Inactive="Neo-ghnìomhach" +Start="Tòisich" +Stop="Cuir stad air" + +Captions="Fo-thiotalan (deuchainneil)" +Captions.AudioSource="Tùs fuaime" +Captions.CurrentSystemLanguage="Cànan làithreach an t-siostaim (%1)" +Captions.Provider="Solaraiche" +Captions.Error.GenericFail="Cha deach leinn na fo-thiotalan a thòiseachadh" + +OutputTimer="Tìmear an às-chuir" +OutputTimer.Stream="Cuir stad air an t-sruthadh às dèidh:" +OutputTimer.Record="Cuir stad air a’ chlàradh às dèidh:" +OutputTimer.Stream.StoppingIn="Thèid an sruthadh a chur ’na stad às dèidh:" +OutputTimer.Record.StoppingIn="Thèid an clàradh a chur ’na stad às dèidh:" +OutputTimer.Stream.EnableEverytime="Cuir tìmear an t-sruthaidh an comas gach turas" +OutputTimer.Record.EnableEverytime="Cuir tìmear a’ chlàraidh an comas gach turas" + +Scripts="Sgriobtaichean" +LoadedScripts="Sgriobtaichean luchdaichte" +AddScripts="Cuir sgriobtaichean ris" +RemoveScripts="Thoir sgriobtaichean air falbh" +ReloadScripts="Ath-luchdaich na sgriobtaichean" +PythonSettings="Roghainnean Python" +PythonSettings.PythonInstallPath32bit="Slighe stàlaidh Python (32 biod)" +PythonSettings.PythonInstallPath64bit="Slighe stàlaidh Python (64 biod)" +PythonSettings.BrowsePythonPath="Rùraich airson slighe stàlaidh Python" +ScriptLogWindow="Loga nan sgriobt" +Description="Tuairisgeul" + +FileFilter.ScriptFiles="Faidhlichean sgriobt" +FileFilter.AllFiles="Na h-uile faidhle" + diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ms-MY.ini b/UI/frontend-plugins/frontend-tools/data/locale/ms-MY.ini index 18ab93e..2de18fa 100644 --- a/UI/frontend-plugins/frontend-tools/data/locale/ms-MY.ini +++ b/UI/frontend-plugins/frontend-tools/data/locale/ms-MY.ini @@ -1,13 +1,28 @@ +SceneSwitcher.OnNoMatch.DontSwitch="Tidak perlu tukar" +SceneSwitcher.OnNoMatch.SwitchTo="Tukar ke:" Active="Aktif" Inactive="Tidak Aktif" Start="Mula" Stop="Berhenti" +Captions="Kapsyen (eksperimen)" +Captions.AudioSource="Sumber audio" +Captions.CurrentSystemLanguage="Sistem bahasa sekarang(%1)" +Captions.Provider="Provider" OutputTimer.Stream="Berhenti 'streaming' selepas:" OutputTimer.Record="Berhenti merakam selepas:" OutputTimer.Stream.StoppingIn="'Streaming' dihentikan dalam:" OutputTimer.Record.StoppingIn="Rakaman dihentikan dalam:" +Scripts="Skrip" +AddScripts="Tambah skrip" +RemoveScripts="Keluarkan skrip" +PythonSettings="Setting Python" +PythonSettings.PythonInstallPath32bit="Python memasang laluan (32 bit)" +PythonSettings.PythonInstallPath64bit="Python memasang laluan (64 bit)" +Description="Huraian" +FileFilter.ScriptFiles="Fail skrip" +FileFilter.AllFiles="Semua fail" diff --git a/UI/frontend-plugins/frontend-tools/data/locale/vi-VN.ini b/UI/frontend-plugins/frontend-tools/data/locale/vi-VN.ini index bbaac9c..6611d79 100644 --- a/UI/frontend-plugins/frontend-tools/data/locale/vi-VN.ini +++ b/UI/frontend-plugins/frontend-tools/data/locale/vi-VN.ini @@ -4,6 +4,8 @@ SceneSwitcher.OnNoMatch.DontSwitch="Không chuyển" SceneSwitcher.OnNoMatch.SwitchTo="Chuyển sang:" SceneSwitcher.CheckInterval="Kiểm tra tiêu đề cửa sổ mỗi:" SceneSwitcher.ActiveOrNotActive="Chuyển cảnh đang:" +InvalidRegex.Title="Cụm từ thông dụng không hợp lệ" +InvalidRegex.Text="Cụm từ thông dụng mà bạn đã nhập không hợp lệ." Active="Đang hoạt động" Inactive="Không hoạt động" Start="Bắt đầu" @@ -15,12 +17,24 @@ Captions.CurrentSystemLanguage="Ngôn ngữ hiện tại của máy tính (%1)" Captions.Provider="Nhà cung cấp" Captions.Error.GenericFail="Thất bại trong việc bắt đầu phụ đề" +OutputTimer="Hẹn giờ đầu ra" OutputTimer.Stream="Dừng stream sau:" OutputTimer.Record="Dừng ghi video sau:" OutputTimer.Stream.StoppingIn="Stream sẽ dừng trong:" OutputTimer.Record.StoppingIn="Quay video sẽ dừng trong:" +OutputTimer.Stream.EnableEverytime="Bật hẹn giờ phát trực tuyến mỗi lần" +OutputTimer.Record.EnableEverytime="Bật hẹn giờ phát mỗi lần" +Scripts="Kịch bản" +LoadedScripts="Kịch bản đã nạp" AddScripts="Thêm script" +RemoveScripts="Gỡ bỏ kịch bản" +ReloadScripts="Nạp lại kịch bản" +PythonSettings="Thiết lập Python" +PythonSettings.PythonInstallPath32bit="Đường dẫn cài đặt Python (32bit)" +PythonSettings.PythonInstallPath64bit="Đường dẫn cài đặt Python (64bit)" +PythonSettings.BrowsePythonPath="Duyệt đường dẫn Python" +ScriptLogWindow="Bản ghi kịch bản" Description="Mô tả" FileFilter.ScriptFiles="Tập tin script" diff --git a/UI/frontend-plugins/frontend-tools/data/locale/zh-CN.ini b/UI/frontend-plugins/frontend-tools/data/locale/zh-CN.ini index 274a488..316d412 100644 --- a/UI/frontend-plugins/frontend-tools/data/locale/zh-CN.ini +++ b/UI/frontend-plugins/frontend-tools/data/locale/zh-CN.ini @@ -11,10 +11,10 @@ Inactive="未激活" Start="开始" Stop="停止" -Captions="标题(实验)" +Captions="字幕 (实验)" Captions.AudioSource="音频源" Captions.CurrentSystemLanguage="当前系统语言 (%1)" -Captions.Provider="供应商" +Captions.Provider="提供程序" Captions.Error.GenericFail="启动捕获失败" OutputTimer="输出计时器" diff --git a/UI/frontend-plugins/frontend-tools/scripts.cpp b/UI/frontend-plugins/frontend-tools/scripts.cpp index d923dcf..364757e 100644 --- a/UI/frontend-plugins/frontend-tools/scripts.cpp +++ b/UI/frontend-plugins/frontend-tools/scripts.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include diff --git a/UI/horizontal-scroll-area.cpp b/UI/horizontal-scroll-area.cpp new file mode 100644 index 0000000..8f927fc --- /dev/null +++ b/UI/horizontal-scroll-area.cpp @@ -0,0 +1,10 @@ +#include +#include "horizontal-scroll-area.hpp" + +void HScrollArea::resizeEvent(QResizeEvent *event) +{ + if (!!widget()) + widget()->setMaximumHeight(event->size().height()); + + QScrollArea::resizeEvent(event); +} diff --git a/UI/horizontal-scroll-area.hpp b/UI/horizontal-scroll-area.hpp new file mode 100644 index 0000000..8a64c3e --- /dev/null +++ b/UI/horizontal-scroll-area.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +class QResizeEvent; + +class HScrollArea : public QScrollArea { + Q_OBJECT + +public: + inline HScrollArea(QWidget *parent = nullptr) + : QScrollArea(parent) + { + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + } + +protected: + virtual void resizeEvent(QResizeEvent *event) override; +}; diff --git a/UI/item-widget-helpers.hpp b/UI/item-widget-helpers.hpp index 9a4d43e..776c2fb 100644 --- a/UI/item-widget-helpers.hpp +++ b/UI/item-widget-helpers.hpp @@ -28,3 +28,15 @@ class QListWidgetItem; QListWidgetItem *TakeListItem(QListWidget *widget, int row); void DeleteListItem(QListWidget *widget, QListWidgetItem *item); void ClearListItems(QListWidget *widget); + +template +void InsertQObjectByName(std::vector &controls, QObjectPtr control) +{ + QString name = control->objectName(); + auto finder = [name](QObjectPtr elem) { + return elem->objectName() > name; + }; + auto found_at = std::find_if(controls.begin(), controls.end(), finder); + + controls.insert(found_at, control); +} diff --git a/UI/obs-app.cpp b/UI/obs-app.cpp index 503e5e1..52b15a0 100644 --- a/UI/obs-app.cpp +++ b/UI/obs-app.cpp @@ -24,9 +24,10 @@ #include #include #include -#include +#include #include #include +#include #include #include @@ -77,8 +78,9 @@ string opt_starting_collection; string opt_starting_profile; string opt_starting_scene; -// AMD PowerXpress High Performance Flags +// GPU hint exports for AMD/NVIDIA laptops #ifdef _MSC_VER +extern "C" __declspec(dllexport) DWORD NvOptimusEnablement = 1; extern "C" __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; #endif @@ -345,6 +347,7 @@ static void do_log(int log_level, const char *msg, va_list args, void *param) } #else def_log_handler(log_level, msg, args2, nullptr); + va_end(args2); #endif if (log_level <= LOG_INFO || log_verbose) { @@ -366,6 +369,7 @@ bool OBSApp::InitGlobalConfigDefaults() config_set_default_string(globalConfig, "General", "Language", DEFAULT_LANG); config_set_default_uint(globalConfig, "General", "MaxLogs", 10); + config_set_default_int(globalConfig, "General", "InfoIncrement", -1); config_set_default_string(globalConfig, "General", "ProcessPriority", "Normal"); config_set_default_bool(globalConfig, "General", "EnableAutoUpdates", @@ -418,9 +422,25 @@ bool OBSApp::InitGlobalConfigDefaults() "CurrentTheme", "Dark"); } + config_set_default_bool(globalConfig, "BasicWindow", + "VerticalVolControl", false); + + config_set_default_bool(globalConfig, "BasicWindow", + "MultiviewMouseSwitch", true); + + config_set_default_bool(globalConfig, "BasicWindow", + "MultiviewDrawNames", true); + + config_set_default_bool(globalConfig, "BasicWindow", + "MultiviewDrawAreas", true); + #ifdef _WIN32 + uint32_t winver = GetWindowsVersion(); + config_set_default_bool(globalConfig, "Audio", "DisableAudioDucking", true); + config_set_default_bool(globalConfig, "General", "BrowserHWAccel", + winver > 0x601); #endif #ifdef __APPLE__ @@ -588,6 +608,42 @@ static string GetSceneCollectionFileFromName(const char *name) return outputPath; } +bool OBSApp::UpdatePre22MultiviewLayout(const char *layout) +{ + if (!layout) + return false; + + if (astrcmpi(layout, "horizontaltop") == 0) { + config_set_int(globalConfig, "BasicWindow", "MultiviewLayout", + static_cast( + MultiviewLayout::HORIZONTAL_TOP_8_SCENES)); + return true; + } + + if (astrcmpi(layout, "horizontalbottom") == 0) { + config_set_int(globalConfig, "BasicWindow", "MultiviewLayout", + static_cast( + MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES)); + return true; + } + + if (astrcmpi(layout, "verticalleft") == 0) { + config_set_int(globalConfig, "BasicWindow", "MultiviewLayout", + static_cast( + MultiviewLayout::VERTICAL_LEFT_8_SCENES)); + return true; + } + + if (astrcmpi(layout, "verticalright") == 0) { + config_set_int(globalConfig, "BasicWindow", "MultiviewLayout", + static_cast( + MultiviewLayout::VERTICAL_RIGHT_8_SCENES)); + return true; + } + + return false; +} + bool OBSApp::InitGlobalConfig() { char path[512]; @@ -653,6 +709,13 @@ bool OBSApp::InitGlobalConfig() changed = true; } + if (config_has_user_value(globalConfig, "BasicWindow", + "MultiviewLayout")) { + const char *layout = config_get_string(globalConfig, + "BasicWindow", "MultiviewLayout"); + changed |= UpdatePre22MultiviewLayout(layout); + } + if (changed) config_save_safe(globalConfig, "tmp", nullptr); @@ -727,6 +790,192 @@ bool OBSApp::InitLocale() return true; } +void OBSApp::AddExtraThemeColor(QPalette &pal, int group, + const char *name, uint32_t color) +{ + std::function func; + +#define DEF_PALETTE_ASSIGN(name) \ + do { \ + func = [&] (QPalette::ColorGroup group) \ + { \ + pal.setColor(group, QPalette::name, \ + QColor::fromRgb(color)); \ + }; \ + } while (false) + + if (astrcmpi(name, "alternateBase") == 0) { + DEF_PALETTE_ASSIGN(AlternateBase); + } else if (astrcmpi(name, "base") == 0) { + DEF_PALETTE_ASSIGN(Base); + } else if (astrcmpi(name, "brightText") == 0) { + DEF_PALETTE_ASSIGN(BrightText); + } else if (astrcmpi(name, "button") == 0) { + DEF_PALETTE_ASSIGN(Button); + } else if (astrcmpi(name, "buttonText") == 0) { + DEF_PALETTE_ASSIGN(ButtonText); + } else if (astrcmpi(name, "brightText") == 0) { + DEF_PALETTE_ASSIGN(BrightText); + } else if (astrcmpi(name, "dark") == 0) { + DEF_PALETTE_ASSIGN(Dark); + } else if (astrcmpi(name, "highlight") == 0) { + DEF_PALETTE_ASSIGN(Highlight); + } else if (astrcmpi(name, "highlightedText") == 0) { + DEF_PALETTE_ASSIGN(HighlightedText); + } else if (astrcmpi(name, "light") == 0) { + DEF_PALETTE_ASSIGN(Light); + } else if (astrcmpi(name, "link") == 0) { + DEF_PALETTE_ASSIGN(Link); + } else if (astrcmpi(name, "linkVisited") == 0) { + DEF_PALETTE_ASSIGN(LinkVisited); + } else if (astrcmpi(name, "mid") == 0) { + DEF_PALETTE_ASSIGN(Mid); + } else if (astrcmpi(name, "midlight") == 0) { + DEF_PALETTE_ASSIGN(Midlight); + } else if (astrcmpi(name, "shadow") == 0) { + DEF_PALETTE_ASSIGN(Shadow); + } else if (astrcmpi(name, "text") == 0 || + astrcmpi(name, "foreground") == 0) { + DEF_PALETTE_ASSIGN(Text); + } else if (astrcmpi(name, "toolTipBase") == 0) { + DEF_PALETTE_ASSIGN(ToolTipBase); + } else if (astrcmpi(name, "toolTipText") == 0) { + DEF_PALETTE_ASSIGN(ToolTipText); + } else if (astrcmpi(name, "windowText") == 0) { + DEF_PALETTE_ASSIGN(WindowText); + } else if (astrcmpi(name, "window") == 0 || + astrcmpi(name, "background") == 0) { + DEF_PALETTE_ASSIGN(Window); + } else { + return; + } + +#undef DEF_PALETTE_ASSIGN + + switch (group) { + case QPalette::Disabled: + case QPalette::Active: + case QPalette::Inactive: + func((QPalette::ColorGroup)group); + break; + default: + func((QPalette::ColorGroup)QPalette::Disabled); + func((QPalette::ColorGroup)QPalette::Active); + func((QPalette::ColorGroup)QPalette::Inactive); + } +} + +struct CFParser { + cf_parser cfp = {}; + inline ~CFParser() {cf_parser_free(&cfp);} + inline operator cf_parser*() {return &cfp;} + inline cf_parser *operator->() {return &cfp;} +}; + +void OBSApp::ParseExtraThemeData(const char *path) +{ + BPtr data = os_quick_read_utf8_file(path); + QPalette pal = palette(); + CFParser cfp; + int ret; + + cf_parser_parse(cfp, data, path); + + while (cf_go_to_token(cfp, "OBSTheme", nullptr)) { + if (!cf_next_token(cfp)) return; + + int group = -1; + + if (cf_token_is(cfp, ":")) { + ret = cf_next_token_should_be(cfp, ":", nullptr, + nullptr); + if (ret != PARSE_SUCCESS) continue; + + if (!cf_next_token(cfp)) return; + + if (cf_token_is(cfp, "disabled")) { + group = QPalette::Disabled; + } else if (cf_token_is(cfp, "active")) { + group = QPalette::Active; + } else if (cf_token_is(cfp, "inactive")) { + group = QPalette::Inactive; + } else { + continue; + } + + if (!cf_next_token(cfp)) return; + } + + if (!cf_token_is(cfp, "{")) continue; + + for (;;) { + if (!cf_next_token(cfp)) return; + + ret = cf_token_is_type(cfp, CFTOKEN_NAME, "name", + nullptr); + if (ret != PARSE_SUCCESS) + break; + + DStr name; + dstr_copy_strref(name, &cfp->cur_token->str); + + ret = cf_next_token_should_be(cfp, ":", ";", + nullptr); + if (ret != PARSE_SUCCESS) continue; + + if (!cf_next_token(cfp)) return; + + const char *array; + uint32_t color = 0; + + if (cf_token_is(cfp, "#")) { + array = cfp->cur_token->str.array; + color = strtol(array + 1, nullptr, 16); + + } else if (cf_token_is(cfp, "rgb")) { + ret = cf_next_token_should_be(cfp, "(", ";", + nullptr); + if (ret != PARSE_SUCCESS) continue; + if (!cf_next_token(cfp)) return; + + array = cfp->cur_token->str.array; + color |= strtol(array, nullptr, 10) << 16; + + ret = cf_next_token_should_be(cfp, ",", ";", + nullptr); + if (ret != PARSE_SUCCESS) continue; + if (!cf_next_token(cfp)) return; + + array = cfp->cur_token->str.array; + color |= strtol(array, nullptr, 10) << 8; + + ret = cf_next_token_should_be(cfp, ",", ";", + nullptr); + if (ret != PARSE_SUCCESS) continue; + if (!cf_next_token(cfp)) return; + + array = cfp->cur_token->str.array; + color |= strtol(array, nullptr, 10); + + } else if (cf_token_is(cfp, "white")) { + color = 0xFFFFFF; + + } else if (cf_token_is(cfp, "black")) { + color = 0; + } + + if (!cf_go_to_token(cfp, ";", nullptr)) return; + + AddExtraThemeColor(pal, group, name->array, color); + } + + ret = cf_token_should_be(cfp, "}", "}", nullptr); + if (ret != PARSE_SUCCESS) continue; + } + + setPalette(pal); +} + bool OBSApp::SetTheme(std::string name, std::string path) { theme = name; @@ -748,12 +997,18 @@ bool OBSApp::SetTheme(std::string name, std::string path) } QString mpath = QString("file:///") + path.c_str(); + setPalette(defaultPalette); setStyleSheet(mpath); + ParseExtraThemeData(path.c_str()); + + emit StyleChanged(); return true; } bool OBSApp::InitTheme() { + defaultPalette = palette(); + const char *themeName = config_get_string(globalConfig, "General", "CurrentTheme"); if (!themeName) { @@ -916,6 +1171,9 @@ void OBSApp::AppInit() EnableOSXVSync(false); #endif + enableHotkeysInFocus = !config_get_bool(globalConfig, "General", + "DisableHotkeysInFocus"); + move_basic_to_profiles(); move_basic_to_scene_collections(); @@ -942,6 +1200,18 @@ static bool StartupOBS(const char *locale, profiler_name_store_t *store) return obs_startup(locale, path, store); } +inline void OBSApp::ResetHotkeyState(bool inFocus) +{ + obs_hotkey_enable_background_press( + inFocus || enableHotkeysInFocus); +} + +void OBSApp::EnableInFocusHotkeys(bool enable) +{ + enableHotkeysInFocus = enable; + ResetHotkeyState(applicationState() != Qt::ApplicationActive); +} + bool OBSApp::OBSInit() { ProfileScope("OBSApp::OBSInit"); @@ -960,6 +1230,19 @@ bool OBSApp::OBSInit() if (!StartupOBS(locale.c_str(), GetProfilerNameStore())) return false; +#ifdef _WIN32 + bool browserHWAccel = config_get_bool(globalConfig, "General", + "BrowserHWAccel"); + + obs_data_t *settings = obs_data_create(); + obs_data_set_bool(settings, "BrowserHWAccel", browserHWAccel); + obs_apply_private_data(settings); + obs_data_release(settings); + + blog(LOG_INFO, "Browser Hardware Acceleration: %s", + browserHWAccel ? "true" : "false"); +#endif + blog(LOG_INFO, "Portable mode: %s", portable_mode ? "true" : "false"); @@ -973,13 +1256,12 @@ bool OBSApp::OBSInit() mainWindow->OBSInit(); connect(this, &QGuiApplication::applicationStateChanged, - [](Qt::ApplicationState state) + [this](Qt::ApplicationState state) { - obs_hotkey_enable_background_press( + ResetHotkeyState( state != Qt::ApplicationActive); }); - obs_hotkey_enable_background_press( - applicationState() != Qt::ApplicationActive); + ResetHotkeyState(applicationState() != Qt::ApplicationActive); return true; } else { return false; @@ -1002,7 +1284,9 @@ string OBSApp::GetVersionString() const #ifdef _WIN32 if (sizeof(void*) == 8) - ver << "64bit, "; + ver << "64-bit, "; + else + ver << "32-bit, "; ver << "windows)"; #elif __APPLE__ @@ -1366,33 +1650,32 @@ static int run_program(fstream &logFile, int argc, char *argv[]) OBSApp program(argc, argv, profilerNameStore.get()); try { + bool created_log = false; + program.AppInit(); - - OBSTranslator translator; - - create_log_file(logFile); delete_oldest_file(false, "obs-studio/profiler_data"); + OBSTranslator translator; program.installTranslator(&translator); #ifdef _WIN32 /* --------------------------------------- */ /* check and warn if already running */ + bool cancel_launch = false; bool already_running = false; RunOnceMutex rom = GetRunOnceMutex(already_running); - if (already_running && !multi) { - blog(LOG_WARNING, "\n================================"); - blog(LOG_WARNING, "Warning: OBS is already running!"); - blog(LOG_WARNING, "================================\n"); + if (!already_running) { + goto run; + } + if (!multi) { QMessageBox::StandardButtons buttons( QMessageBox::Yes | QMessageBox::Cancel); QMessageBox mb(QMessageBox::Question, QTStr("AlreadyRunning.Title"), - QTStr("AlreadyRunning.Text"), - buttons, + QTStr("AlreadyRunning.Text"), buttons, nullptr); mb.setButtonText(QMessageBox::Yes, QTStr("AlreadyRunning.LaunchAnyway")); @@ -1401,23 +1684,37 @@ static int run_program(fstream &logFile, int argc, char *argv[]) QMessageBox::StandardButton button; button = (QMessageBox::StandardButton)mb.exec(); - if (button == QMessageBox::Cancel) { - blog(LOG_INFO, "User shut down the program " - "because OBS was already " - "running"); - return 0; - } + cancel_launch = button == QMessageBox::Cancel; + } - blog(LOG_WARNING, "User is now running a secondary " - "instance of OBS!"); + if (cancel_launch) + return 0; - } else if (already_running && multi) { + if (!created_log) { + create_log_file(logFile); + created_log = true; + } + + if (multi) { blog(LOG_INFO, "User enabled --multi flag and is now " "running multiple instances of OBS."); + } else { + blog(LOG_WARNING, "================================"); + blog(LOG_WARNING, "Warning: OBS is already running!"); + blog(LOG_WARNING, "================================"); + blog(LOG_WARNING, "User is now running multiple " + "instances of OBS!"); } /* --------------------------------------- */ +run: #endif + + if (!created_log) { + create_log_file(logFile); + created_log = true; + } + if (argc > 1) { stringstream stor; stor << argv[1]; @@ -1887,6 +2184,7 @@ int main(int argc, char *argv[]) #endif #ifdef _WIN32 + obs_init_win32_crash_handler(); SetErrorMode(SEM_FAILCRITICALERRORS); load_debug_privilege(); base_set_crash_handler(main_crash_handler, nullptr); diff --git a/UI/obs-app.hpp b/UI/obs-app.hpp index 87d6a1c..9fd7448 100644 --- a/UI/obs-app.hpp +++ b/UI/obs-app.hpp @@ -73,13 +73,26 @@ private: os_inhibit_t *sleepInhibitor = nullptr; int sleepInhibitRefs = 0; + bool enableHotkeysInFocus = true; + + std::deque translatorHooks; + bool UpdatePre22MultiviewLayout(const char *layout); + bool InitGlobalConfig(); bool InitGlobalConfigDefaults(); bool InitLocale(); bool InitTheme(); + inline void ResetHotkeyState(bool inFocus); + + QPalette defaultPalette; + + void ParseExtraThemeData(const char *path); + void AddExtraThemeColor(QPalette &pal, int group, + const char *name, uint32_t color); + public: OBSApp(int &argc, char **argv, profiler_name_store_t *store); ~OBSApp(); @@ -87,6 +100,8 @@ public: void AppInit(); bool OBSInit(); + void EnableInFocusHotkeys(bool enable); + inline QMainWindow *GetMainWindow() const {return mainWindow.data();} inline config_t *GlobalConfig() const {return globalConfig;} @@ -150,6 +165,9 @@ public: { translatorHooks.pop_front(); } + +signals: + void StyleChanged(); }; int GetConfigPath(char *path, size_t size, const char *name); diff --git a/UI/obs-frontend-api/obs-frontend-api.cpp b/UI/obs-frontend-api/obs-frontend-api.cpp index ee47fff..c3c933a 100644 --- a/UI/obs-frontend-api/obs-frontend-api.cpp +++ b/UI/obs-frontend-api/obs-frontend-api.cpp @@ -148,6 +148,13 @@ void obs_frontend_set_current_scene_collection(const char *collection) c->obs_frontend_set_current_scene_collection(collection); } +bool obs_frontend_add_scene_collection(const char *name) +{ + return callbacks_valid() + ? c->obs_frontend_add_scene_collection(name) + : false; +} + char **obs_frontend_get_profiles(void) { if (!callbacks_valid()) @@ -297,6 +304,18 @@ void obs_frontend_save(void) c->obs_frontend_save(); } +void obs_frontend_defer_save_begin(void) +{ + if (callbacks_valid()) + c->obs_frontend_defer_save_begin(); +} + +void obs_frontend_defer_save_end(void) +{ + if (callbacks_valid()) + c->obs_frontend_defer_save_end(); +} + void obs_frontend_add_save_callback(obs_frontend_save_cb callback, void *private_data) { diff --git a/UI/obs-frontend-api/obs-frontend-api.h b/UI/obs-frontend-api/obs-frontend-api.h index 05483ff..ae67494 100644 --- a/UI/obs-frontend-api/obs-frontend-api.h +++ b/UI/obs-frontend-api/obs-frontend-api.h @@ -42,7 +42,8 @@ enum obs_frontend_event { OBS_FRONTEND_EVENT_STUDIO_MODE_DISABLED, OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED, - OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP + OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP, + OBS_FRONTEND_EVENT_FINISHED_LOADING }; /* ------------------------------------------------------------------------- */ @@ -95,6 +96,7 @@ EXPORT void obs_frontend_set_current_transition(obs_source_t *transition); EXPORT char **obs_frontend_get_scene_collections(void); EXPORT char *obs_frontend_get_current_scene_collection(void); EXPORT void obs_frontend_set_current_scene_collection(const char *collection); +EXPORT bool obs_frontend_add_scene_collection(const char *name); EXPORT char **obs_frontend_get_profiles(void); EXPORT char *obs_frontend_get_current_profile(void); @@ -150,6 +152,8 @@ EXPORT void obs_frontend_replay_buffer_stop(void); EXPORT bool obs_frontend_replay_buffer_active(void); EXPORT void obs_frontend_save(void); +EXPORT void obs_frontend_defer_save_begin(void); +EXPORT void obs_frontend_defer_save_end(void); EXPORT obs_output_t *obs_frontend_get_streaming_output(void); EXPORT obs_output_t *obs_frontend_get_recording_output(void); diff --git a/UI/obs-frontend-api/obs-frontend-internal.hpp b/UI/obs-frontend-api/obs-frontend-internal.hpp index 0749d2e..b1a9064 100644 --- a/UI/obs-frontend-api/obs-frontend-internal.hpp +++ b/UI/obs-frontend-api/obs-frontend-internal.hpp @@ -26,6 +26,7 @@ struct obs_frontend_callbacks { virtual char *obs_frontend_get_current_scene_collection(void)=0; virtual void obs_frontend_set_current_scene_collection( const char *collection)=0; + virtual bool obs_frontend_add_scene_collection(const char *name)=0; virtual void obs_frontend_get_profiles( std::vector &strings)=0; @@ -61,7 +62,9 @@ struct obs_frontend_callbacks { virtual config_t *obs_frontend_get_profile_config(void)=0; virtual config_t *obs_frontend_get_global_config(void)=0; - virtual void obs_frontend_save(void)=0; + virtual void obs_frontend_save(void) = 0; + virtual void obs_frontend_defer_save_begin(void) = 0; + virtual void obs_frontend_defer_save_end(void) = 0; virtual void obs_frontend_add_save_callback( obs_frontend_save_cb callback, void *private_data)=0; virtual void obs_frontend_remove_save_callback( diff --git a/UI/platform-osx.mm b/UI/platform-osx.mm index 19b1a9c..37e7c0a 100644 --- a/UI/platform-osx.mm +++ b/UI/platform-osx.mm @@ -168,3 +168,11 @@ void EnableOSXVSync(bool enable) deferred_updates(enable ? 1 : 0); } } + +void EnableOSXDockIcon(bool enable) +{ + if (enable) + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + else + [NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited]; +} diff --git a/UI/platform.hpp b/UI/platform.hpp index 5011e60..4da23b3 100644 --- a/UI/platform.hpp +++ b/UI/platform.hpp @@ -63,4 +63,5 @@ RunOnceMutex GetRunOnceMutex(bool &already_running); #ifdef __APPLE__ void EnableOSXVSync(bool enable); +void EnableOSXDockIcon(bool enable); #endif diff --git a/UI/properties-view.cpp b/UI/properties-view.cpp index 169b9f7..4a286b6 100644 --- a/UI/properties-view.cpp +++ b/UI/properties-view.cpp @@ -576,6 +576,10 @@ void OBSPropertiesView::AddEditableList(obs_property_t *prop, for (size_t i = 0; i < count; i++) { obs_data_t *item = obs_data_array_item(array, i); list->addItem(QT_UTF8(obs_data_get_string(item, "value"))); + list->setItemSelected(list->item((int)i), + obs_data_get_bool(item, "selected")); + list->setItemHidden(list->item((int)i), + obs_data_get_bool(item, "hidden")); obs_data_release(item); } @@ -634,9 +638,16 @@ void OBSPropertiesView::AddColor(obs_property_t *prop, QFormLayout *layout, button->setText(QTStr("Basic.PropertiesWindow.SelectColor")); button->setToolTip(QT_UTF8(obs_property_long_description(prop))); + color.setAlpha(255); + + QPalette palette = QPalette(color); colorLabel->setFrameStyle(QFrame::Sunken | QFrame::Panel); colorLabel->setText(color.name(QColor::HexArgb)); - colorLabel->setPalette(QPalette(color)); + colorLabel->setPalette(palette); + colorLabel->setStyleSheet( + QString("background-color :%1; color: %2;") + .arg(palette.color(QPalette::Window).name(QColor::HexArgb)) + .arg(palette.color(QPalette::WindowText).name(QColor::HexArgb))); colorLabel->setAutoFillBackground(true); colorLabel->setAlignment(Qt::AlignCenter); colorLabel->setToolTip(QT_UTF8(obs_property_long_description(prop))); @@ -1626,13 +1637,19 @@ bool WidgetInfo::ColorChanged(const char *setting) #endif color = QColorDialog::getColor(color, view, QT_UTF8(desc), options); + color.setAlpha(255); if (!color.isValid()) return false; QLabel *label = static_cast(widget); label->setText(color.name(QColor::HexArgb)); - label->setPalette(QPalette(color)); + QPalette palette = QPalette(color); + label->setPalette(palette); + label->setStyleSheet( + QString("background-color :%1; color: %2;") + .arg(palette.color(QPalette::Window).name(QColor::HexArgb)) + .arg(palette.color(QPalette::WindowText).name(QColor::HexArgb))); obs_data_set_int(view->settings, setting, color_to_int(color)); @@ -1646,11 +1663,18 @@ bool WidgetInfo::FontChanged(const char *setting) uint32_t flags; QFont font; + QFontDialog::FontDialogOptions options; + +#ifdef __APPLE__ + options = QFontDialog::DontUseNativeDialog; +#endif + if (!font_obj) { - font = QFontDialog::getFont(&success, view); + QFont initial; + font = QFontDialog::getFont(&success, initial, view, "Pick a Font", options); } else { MakeQFont(font_obj, font); - font = QFontDialog::getFont(&success, font, view); + font = QFontDialog::getFont(&success, font, view, "Pick a Font", options); obs_data_release(font_obj); } @@ -1690,7 +1714,10 @@ void WidgetInfo::EditableListChanged() obs_data_t *arrayItem = obs_data_create(); obs_data_set_string(arrayItem, "value", QT_TO_UTF8(item->text())); - + obs_data_set_bool(arrayItem, "selected", + item->isSelected()); + obs_data_set_bool(arrayItem, "hidden", + item->isHidden()); obs_data_array_push_back(array, arrayItem); obs_data_release(arrayItem); } diff --git a/UI/qt-wrappers.hpp b/UI/qt-wrappers.hpp index 7899387..5273f92 100644 --- a/UI/qt-wrappers.hpp +++ b/UI/qt-wrappers.hpp @@ -17,8 +17,10 @@ #pragma once +#include #include #include +#include #include #include @@ -79,3 +81,10 @@ public: }; void DeleteLayout(QLayout *layout); + +static inline Qt::ConnectionType WaitConnection() +{ + return QThread::currentThread() == qApp->thread() + ? Qt::DirectConnection + : Qt::BlockingQueuedConnection; +} diff --git a/UI/slider-absoluteset-style.hpp b/UI/slider-absoluteset-style.hpp index a94ebeb..d275ad2 100644 --- a/UI/slider-absoluteset-style.hpp +++ b/UI/slider-absoluteset-style.hpp @@ -6,7 +6,7 @@ class SliderAbsoluteSetStyle : public QProxyStyle { public: SliderAbsoluteSetStyle(const QString& baseStyle); - SliderAbsoluteSetStyle(QStyle* baseStyle); + SliderAbsoluteSetStyle(QStyle* baseStyle = Q_NULLPTR); int styleHint(QStyle::StyleHint hint, const QStyleOption* option, const QWidget* widget, QStyleHintReturn* returnData) const; }; diff --git a/UI/source-tree.cpp b/UI/source-tree.cpp new file mode 100644 index 0000000..1652988 --- /dev/null +++ b/UI/source-tree.cpp @@ -0,0 +1,1390 @@ +#include "window-basic-main.hpp" +#include "obs-app.hpp" +#include "source-tree.hpp" +#include "qt-wrappers.hpp" +#include "visibility-checkbox.hpp" +#include "locked-checkbox.hpp" +#include "expand-checkbox.hpp" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static inline OBSScene GetCurrentScene() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + return main->GetCurrentScene(); +} + +/* ========================================================================= */ + +SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) + : tree (tree_), + sceneitem (sceneitem_) +{ + setAttribute(Qt::WA_TranslucentBackground); + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + const char *name = obs_source_get_name(source); + + obs_data_t *privData = obs_sceneitem_get_private_settings(sceneitem); + int preset = obs_data_get_int(privData, "color-preset"); + + if (preset == 1) { + const char *color = obs_data_get_string(privData, "color"); + std::string col = "background: "; + col += color; + setStyleSheet(col.c_str()); + } else if (preset > 1) { + setStyleSheet(""); + setProperty("bgColor", preset - 1); + } else { + setStyleSheet("background: none"); + } + + obs_data_release(privData); + + vis = new VisibilityCheckBox(); + vis->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + vis->setMaximumSize(16, 16); + vis->setChecked(obs_sceneitem_visible(sceneitem)); + + lock = new LockedCheckBox(); + lock->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + lock->setMaximumSize(16, 16); + lock->setChecked(obs_sceneitem_locked(sceneitem)); + + label = new QLabel(QT_UTF8(name)); + label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + label->setAttribute(Qt::WA_TranslucentBackground); + +#ifdef __APPLE__ + vis->setAttribute(Qt::WA_LayoutUsesWidgetRect); + lock->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + boxLayout = new QHBoxLayout(); + boxLayout->setContentsMargins(1, 1, 2, 1); + boxLayout->setSpacing(1); + boxLayout->addWidget(label); + boxLayout->addWidget(vis); + boxLayout->addWidget(lock); +#ifdef __APPLE__ + /* Hack: Fixes a bug where scrollbars would be above the lock icon */ + boxLayout->addSpacing(16); +#endif + + Update(false); + + setLayout(boxLayout); + + /* --------------------------------------------------------- */ + + auto setItemVisible = [this] (bool checked) + { + SignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_visible(sceneitem, checked); + }; + + auto setItemLocked = [this] (bool checked) + { + SignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_locked(sceneitem, checked); + }; + + connect(vis, &QAbstractButton::clicked, setItemVisible); + connect(lock, &QAbstractButton::clicked, setItemLocked); +} + +void SourceTreeItem::paintEvent(QPaintEvent *event) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + + QWidget::paintEvent(event); +} + +void SourceTreeItem::DisconnectSignals() +{ + sceneRemoveSignal.Disconnect(); + itemRemoveSignal.Disconnect(); + deselectSignal.Disconnect(); + visibleSignal.Disconnect(); + renameSignal.Disconnect(); + removeSignal.Disconnect(); +} + +void SourceTreeItem::ReconnectSignals() +{ + if (!sceneitem) + return; + + DisconnectSignals(); + + /* --------------------------------------------------------- */ + + auto removeItem = [] (void *data, calldata_t *cd) + { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = + (obs_sceneitem_t*)calldata_ptr(cd, "item"); + + if (curItem == this_->sceneitem) { + QMetaObject::invokeMethod(this_->tree, + "Remove", + Q_ARG(OBSSceneItem, curItem)); + curItem = nullptr; + } + if (!curItem) { + this_->DisconnectSignals(); + this_->sceneitem = nullptr; + } + }; + + auto itemVisible = [] (void *data, calldata_t *cd) + { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = + (obs_sceneitem_t*)calldata_ptr(cd, "item"); + bool visible = calldata_bool(cd, "visible"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "VisibilityChanged", + Q_ARG(bool, visible)); + }; + + auto itemDeselect = [] (void *data, calldata_t *cd) + { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = + (obs_sceneitem_t*)calldata_ptr(cd, "item"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "Deselect"); + }; + + auto reorderGroup = [] (void *data, calldata_t*) + { + SourceTreeItem *this_ = reinterpret_cast(data); + QMetaObject::invokeMethod(this_->tree, "ReorderItems"); + }; + + obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); + obs_source_t *sceneSource = obs_scene_get_source(scene); + signal_handler_t *signal = obs_source_get_signal_handler(sceneSource); + + sceneRemoveSignal.Connect(signal, "remove", removeItem, this); + itemRemoveSignal.Connect(signal, "item_remove", removeItem, this); + visibleSignal.Connect(signal, "item_visible", itemVisible, this); + + if (obs_sceneitem_is_group(sceneitem)) { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + signal = obs_source_get_signal_handler(source); + + groupReorderSignal.Connect(signal, "reorder", reorderGroup, + this); + } + + if (scene != GetCurrentScene()) + deselectSignal.Connect(signal, "item_deselect", itemDeselect, + this); + + /* --------------------------------------------------------- */ + + auto renamed = [] (void *data, calldata_t *cd) + { + SourceTreeItem *this_ = reinterpret_cast(data); + const char *name = calldata_string(cd, "new_name"); + + QMetaObject::invokeMethod(this_, "Renamed", + Q_ARG(QString, QT_UTF8(name))); + }; + + auto removeSource = [] (void *data, calldata_t *) + { + SourceTreeItem *this_ = reinterpret_cast(data); + this_->DisconnectSignals(); + this_->sceneitem = nullptr; + }; + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + signal = obs_source_get_signal_handler(source); + renameSignal.Connect(signal, "rename", renamed, this); + removeSignal.Connect(signal, "remove", removeSource, this); +} + +void SourceTreeItem::mouseDoubleClickEvent(QMouseEvent *event) +{ + QWidget::mouseDoubleClickEvent(event); + + if (expand) { + expand->setChecked(!expand->isChecked()); + } else { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + OBSBasic *main = + reinterpret_cast(App()->GetMainWindow()); + if (source) { + main->CreatePropertiesWindow(source); + } + } +} + +void SourceTreeItem::EnterEditMode() +{ + setFocusPolicy(Qt::StrongFocus); + boxLayout->removeWidget(label); + editor = new QLineEdit(label->text()); + editor->installEventFilter(this); + boxLayout->insertWidget(1, editor); + setFocusProxy(editor); +} + +void SourceTreeItem::ExitEditMode(bool save) +{ + if (!editor) + return; + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + OBSScene scene = main->GetCurrentScene(); + + std::string newName = QT_TO_UTF8(editor->text()); + + setFocusProxy(nullptr); + boxLayout->removeWidget(editor); + delete editor; + editor = nullptr; + setFocusPolicy(Qt::NoFocus); + boxLayout->insertWidget(1, label); + + /* ----------------------------------------- */ + /* check for empty string */ + + if (!save) + return; + + if (newName.empty()) { + OBSMessageBox::information(main, + QTStr("NoNameEntered.Title"), + QTStr("NoNameEntered.Text")); + return; + } + + /* ----------------------------------------- */ + /* Check for same name */ + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + if (newName == obs_source_get_name(source)) + return; + + /* ----------------------------------------- */ + /* check for existing source */ + + obs_source_t *existingSource = + obs_get_source_by_name(newName.c_str()); + obs_source_release(existingSource); + bool exists = !!existingSource; + + if (exists) { + OBSMessageBox::information(main, + QTStr("NameExists.Title"), + QTStr("NameExists.Text")); + return; + } + + /* ----------------------------------------- */ + /* rename */ + + SignalBlocker sourcesSignalBlocker(this); + obs_source_set_name(source, newName.c_str()); + label->setText(QT_UTF8(newName.c_str())); +} + +bool SourceTreeItem::eventFilter(QObject *object, QEvent *event) +{ + if (editor != object) + return false; + + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + + switch (keyEvent->key()) { + case Qt::Key_Escape: + QMetaObject::invokeMethod(this, "ExitEditMode", + Qt::QueuedConnection, + Q_ARG(bool, false)); + return true; + case Qt::Key_Tab: + case Qt::Key_Backtab: + case Qt::Key_Enter: + case Qt::Key_Return: + QMetaObject::invokeMethod(this, "ExitEditMode", + Qt::QueuedConnection, + Q_ARG(bool, true)); + return true; + } + } else if (event->type() == QEvent::FocusOut) { + QMetaObject::invokeMethod(this, "ExitEditMode", + Qt::QueuedConnection, + Q_ARG(bool, true)); + return true; + } + + return false; +} + +void SourceTreeItem::VisibilityChanged(bool visible) +{ + vis->setChecked(visible); +} + +void SourceTreeItem::Renamed(const QString &name) +{ + label->setText(name); +} + +void SourceTreeItem::Update(bool force) +{ + OBSScene scene = GetCurrentScene(); + obs_scene_t *itemScene = obs_sceneitem_get_scene(sceneitem); + + Type newType; + + /* ------------------------------------------------- */ + /* if it's a group item, insert group checkbox */ + + if (obs_sceneitem_is_group(sceneitem)) { + newType = Type::Group; + + /* ------------------------------------------------- */ + /* if it's a group sub-item */ + + } else if (itemScene != scene) { + newType = Type::SubItem; + + /* ------------------------------------------------- */ + /* if it's a regular item */ + + } else { + newType = Type::Item; + } + + /* ------------------------------------------------- */ + + if (!force && newType == type) { + return; + } + + /* ------------------------------------------------- */ + + ReconnectSignals(); + + if (spacer) { + boxLayout->removeItem(spacer); + delete spacer; + spacer = nullptr; + } + + if (type == Type::Group) { + boxLayout->removeWidget(expand); + expand->deleteLater(); + expand = nullptr; + } + + type = newType; + + if (type == Type::SubItem) { + spacer = new QSpacerItem(16, 1); + boxLayout->insertItem(0, spacer); + + } else if (type == Type::Group) { + expand = new SourceTreeSubItemCheckBox(); + expand->setSizePolicy( + QSizePolicy::Maximum, + QSizePolicy::Maximum); + expand->setMaximumSize(10, 16); + expand->setMinimumSize(10, 0); +#ifdef __APPLE__ + expand->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + boxLayout->insertWidget(0, expand); + + obs_data_t *data = obs_sceneitem_get_private_settings(sceneitem); + expand->blockSignals(true); + expand->setChecked(obs_data_get_bool(data, "collapsed")); + expand->blockSignals(false); + obs_data_release(data); + + connect(expand, &QPushButton::toggled, + this, &SourceTreeItem::ExpandClicked); + + } else { + spacer = new QSpacerItem(3, 1); + boxLayout->insertItem(0, spacer); + } +} + +void SourceTreeItem::ExpandClicked(bool checked) +{ + OBSData data = obs_sceneitem_get_private_settings(sceneitem); + obs_data_release(data); + + obs_data_set_bool(data, "collapsed", checked); + + if (!checked) + tree->GetStm()->ExpandGroup(sceneitem); + else + tree->GetStm()->CollapseGroup(sceneitem); +} + +void SourceTreeItem::Deselect() +{ + tree->SelectItem(sceneitem, false); +} + +/* ========================================================================= */ + +void SourceTreeModel::OBSFrontendEvent(enum obs_frontend_event event, void *ptr) +{ + SourceTreeModel *stm = reinterpret_cast(ptr); + + switch ((int)event) { + case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED: + stm->SceneChanged(); + break; + case OBS_FRONTEND_EVENT_EXIT: + case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP: + stm->Clear(); + break; + } +} + +void SourceTreeModel::Clear() +{ + beginResetModel(); + items.clear(); + endResetModel(); + + hasGroups = false; +} + +static bool enumItem(obs_scene_t*, obs_sceneitem_t *item, void *ptr) +{ + QVector &items = + *reinterpret_cast*>(ptr); + + if (obs_sceneitem_is_group(item)) { + obs_data_t *data = obs_sceneitem_get_private_settings(item); + + bool collapse = obs_data_get_bool(data, "collapsed"); + if (!collapse) { + obs_scene_t *scene = + obs_sceneitem_group_get_scene(item); + + obs_scene_enum_items(scene, enumItem, &items); + } + + obs_data_release(data); + } + + items.insert(0, item); + return true; +} + +void SourceTreeModel::SceneChanged() +{ + OBSScene scene = GetCurrentScene(); + + beginResetModel(); + items.clear(); + obs_scene_enum_items(scene, enumItem, &items); + endResetModel(); + + UpdateGroupState(false); + st->ResetWidgets(); + + for (int i = 0; i < items.count(); i++) { + bool select = obs_sceneitem_selected(items[i]); + QModelIndex index = createIndex(i, 0); + + st->selectionModel()->select(index, select + ? QItemSelectionModel::Select + : QItemSelectionModel::Deselect); + } +} + +/* moves a scene item index (blame linux distros for using older Qt builds) */ +static inline void MoveItem(QVector &items, int oldIdx, int newIdx) +{ + OBSSceneItem item = items[oldIdx]; + items.remove(oldIdx); + items.insert(newIdx, item); +} + +/* reorders list optimally with model reorder funcs */ +void SourceTreeModel::ReorderItems() +{ + OBSScene scene = GetCurrentScene(); + + QVector newitems; + obs_scene_enum_items(scene, enumItem, &newitems); + + /* if item list has changed size, do full reset */ + if (newitems.count() != items.count()) { + SceneChanged(); + return; + } + + for (;;) { + int idx1Old = 0; + int idx1New = 0; + int count; + int i; + + /* find first starting changed item index */ + for (i = 0; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[i]; + obs_sceneitem_t *newItem = newitems[i]; + if (oldItem != newItem) { + idx1Old = i; + break; + } + } + + /* if everything is the same, break */ + if (i == newitems.count()) { + break; + } + + /* find new starting index */ + for (i = idx1Old + 1; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[idx1Old]; + obs_sceneitem_t *newItem = newitems[i]; + + if (oldItem == newItem) { + idx1New = i; + break; + } + } + + /* if item could not be found, do full reset */ + if (i == newitems.count()) { + SceneChanged(); + return; + } + + /* get move count */ + for (count = 1; (idx1New + count) < newitems.count(); count++) { + int oldIdx = idx1Old + count; + int newIdx = idx1New + count; + + obs_sceneitem_t *oldItem = items[oldIdx]; + obs_sceneitem_t *newItem = newitems[newIdx]; + + if (oldItem != newItem) { + break; + } + } + + /* move items */ + beginMoveRows(QModelIndex(), idx1Old, idx1Old + count - 1, + QModelIndex(), idx1New + count); + for (i = 0; i < count; i++) { + int to = idx1New + count; + if (to > idx1Old) + to--; + MoveItem(items, idx1Old, to); + } + endMoveRows(); + } +} + +void SourceTreeModel::Add(obs_sceneitem_t *item) +{ + if (obs_sceneitem_is_group(item)) { + SceneChanged(); + } else { + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, item); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), item); + } +} + +void SourceTreeModel::Remove(obs_sceneitem_t *item) +{ + int idx = -1; + for (int i = 0; i < items.count(); i++) { + if (items[i] == item) { + idx = i; + break; + } + } + + if (idx == -1) + return; + + int startIdx = idx; + int endIdx = idx; + + bool is_group = obs_sceneitem_is_group(item); + if (is_group) { + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = endIdx + 1; i < items.count(); i++) { + obs_sceneitem_t *subitem = items[i]; + obs_scene_t *subscene = + obs_sceneitem_get_scene(subitem); + + if (subscene == scene) + endIdx = i; + else + break; + } + } + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(idx, endIdx - startIdx + 1); + endRemoveRows(); + + if (is_group) + UpdateGroupState(true); +} + +OBSSceneItem SourceTreeModel::Get(int idx) +{ + if (idx == -1 || idx >= items.count()) + return OBSSceneItem(); + return items[idx]; +} + +SourceTreeModel::SourceTreeModel(SourceTree *st_) + : QAbstractListModel (st_), + st (st_) +{ + obs_frontend_add_event_callback(OBSFrontendEvent, this); +} + +SourceTreeModel::~SourceTreeModel() +{ + obs_frontend_remove_event_callback(OBSFrontendEvent, this); +} + +int SourceTreeModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : items.count(); +} + +QVariant SourceTreeModel::data(const QModelIndex &, int) const +{ + return QVariant(); +} + +Qt::ItemFlags SourceTreeModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return QAbstractListModel::flags(index) | Qt::ItemIsDropEnabled; + + obs_sceneitem_t *item = items[index.row()]; + bool is_group = obs_sceneitem_is_group(item); + + return QAbstractListModel::flags(index) | + Qt::ItemIsEditable | + Qt::ItemIsDragEnabled | + (is_group ? Qt::ItemIsDropEnabled : Qt::NoItemFlags); +} + +Qt::DropActions SourceTreeModel::supportedDropActions() const +{ + return QAbstractItemModel::supportedDropActions() | Qt::MoveAction; +} + +QString SourceTreeModel::GetNewGroupName() +{ + OBSScene scene = GetCurrentScene(); + QString name = QTStr("Group"); + + int i = 2; + for (;;) { + obs_source_t *group = obs_get_source_by_name(QT_TO_UTF8(name)); + obs_source_release(group); + if (!group) + break; + name = QTStr("Basic.Main.Group").arg(QString::number(i++)); + } + + return name; +} + +void SourceTreeModel::AddGroup() +{ + QString name = GetNewGroupName(); + obs_sceneitem_t *group = obs_scene_add_group(GetCurrentScene(), + QT_TO_UTF8(name)); + if (!group) + return; + + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, group); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), group); + UpdateGroupState(true); + + QMetaObject::invokeMethod(st, "Edit", Qt::QueuedConnection, + Q_ARG(int, 0)); +} + +void SourceTreeModel::GroupSelectedItems(QModelIndexList &indices) +{ + if (indices.count() == 0) + return; + + OBSScene scene = GetCurrentScene(); + QString name = GetNewGroupName(); + + QVector item_order; + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + item_order << item; + } + + obs_sceneitem_t *item = obs_scene_insert_group( + scene, QT_TO_UTF8(name), + item_order.data(), item_order.size()); + if (!item) { + return; + } + + for (obs_sceneitem_t *item : item_order) + obs_sceneitem_select(item, false); + + int newIdx = indices[0].row(); + + beginInsertRows(QModelIndex(), newIdx, newIdx); + items.insert(newIdx, item); + endInsertRows(); + + for (int i = 0; i < indices.size(); i++) { + int fromIdx = indices[i].row() + 1; + int toIdx = newIdx + i + 1; + if (fromIdx != toIdx) { + beginMoveRows(QModelIndex(), fromIdx, fromIdx, + QModelIndex(), toIdx); + MoveItem(items, fromIdx, toIdx); + endMoveRows(); + } + } + + hasGroups = true; + st->UpdateWidgets(true); + + obs_sceneitem_select(item, true); + + QMetaObject::invokeMethod(st, "Edit", Qt::QueuedConnection, + Q_ARG(int, newIdx)); +} + +void SourceTreeModel::UngroupSelectedGroups(QModelIndexList &indices) +{ + if (indices.count() == 0) + return; + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_sceneitem_group_ungroup(item); + } + + SceneChanged(); +} + +void SourceTreeModel::ExpandGroup(obs_sceneitem_t *item) +{ + int itemIdx = items.indexOf(item); + if (itemIdx == -1) + return; + + itemIdx++; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + QVector subItems; + obs_scene_enum_items(scene, enumItem, &subItems); + + if (!subItems.size()) + return; + + beginInsertRows(QModelIndex(), itemIdx, itemIdx + subItems.size() - 1); + for (int i = 0; i < subItems.size(); i++) + items.insert(i + itemIdx, subItems[i]); + endInsertRows(); + + st->UpdateWidgets(); +} + +void SourceTreeModel::CollapseGroup(obs_sceneitem_t *item) +{ + int startIdx = -1; + int endIdx = -1; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = 0; i < items.size(); i++) { + obs_scene_t *itemScene = obs_sceneitem_get_scene(items[i]); + + if (itemScene == scene) { + if (startIdx == -1) + startIdx = i; + endIdx = i; + } + } + + if (startIdx == -1) + return; + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(startIdx, endIdx - startIdx + 1); + endRemoveRows(); +} + +void SourceTreeModel::UpdateGroupState(bool update) +{ + bool nowHasGroups = false; + for (auto &item : items) { + if (obs_sceneitem_is_group(item)) { + nowHasGroups = true; + break; + } + } + + if (nowHasGroups != hasGroups) { + hasGroups = nowHasGroups; + if (update) { + st->UpdateWidgets(true); + } + } +} + +/* ========================================================================= */ + +SourceTree::SourceTree(QWidget *parent_) : QListView(parent_) +{ + SourceTreeModel *stm_ = new SourceTreeModel(this); + setModel(stm_); + setStyleSheet(QString( + "*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" \ + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" \ + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" \ + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" \ + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" \ + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" \ + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" \ + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); +} + +void SourceTree::ResetWidgets() +{ + OBSScene scene = GetCurrentScene(); + + SourceTreeModel *stm = GetStm(); + stm->UpdateGroupState(false); + + for (int i = 0; i < stm->items.count(); i++) { + QModelIndex index = stm->createIndex(i, 0, nullptr); + setIndexWidget(index, new SourceTreeItem(this, stm->items[i])); + } +} + +void SourceTree::UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item) +{ + setIndexWidget(idx, new SourceTreeItem(this, item)); +} + +void SourceTree::UpdateWidgets(bool force) +{ + SourceTreeModel *stm = GetStm(); + + for (int i = 0; i < stm->items.size(); i++) { + obs_sceneitem_t *item = stm->items[i]; + SourceTreeItem *widget = GetItemWidget(i); + + if (!widget) { + UpdateWidget(stm->createIndex(i, 0), item); + } else { + widget->Update(force); + } + } +} + +void SourceTree::SelectItem(obs_sceneitem_t *sceneitem, bool select) +{ + SourceTreeModel *stm = GetStm(); + int i = 0; + + for (; i < stm->items.count(); i++) { + if (stm->items[i] == sceneitem) + break; + } + + if (i == stm->items.count()) + return; + + QModelIndex index = stm->createIndex(i, 0); + if (index.isValid()) + selectionModel()->select(index, select + ? QItemSelectionModel::Select + : QItemSelectionModel::Deselect); +} + +Q_DECLARE_METATYPE(OBSSceneItem); + +void SourceTree::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) + QListView::mouseDoubleClickEvent(event); +} + +void SourceTree::dropEvent(QDropEvent *event) +{ + if (event->source() != this) { + QListView::dropEvent(event); + return; + } + + OBSScene scene = GetCurrentScene(); + SourceTreeModel *stm = GetStm(); + auto &items = stm->items; + QModelIndexList indices = selectedIndexes(); + + DropIndicatorPosition indicator = dropIndicatorPosition(); + int row = indexAt(event->pos()).row(); + bool emptyDrop = row == -1; + + if (emptyDrop) { + if (!items.size()) { + QListView::dropEvent(event); + return; + } + + row = items.size() - 1; + indicator = QAbstractItemView::BelowItem; + } + + /* --------------------------------------- */ + /* store destination group if moving to a */ + /* group */ + + obs_sceneitem_t *dropItem = items[row]; /* item being dropped on */ + bool itemIsGroup = obs_sceneitem_is_group(dropItem); + + obs_sceneitem_t *dropGroup = itemIsGroup + ? dropItem + : obs_sceneitem_get_group(scene, dropItem); + + /* not a group if moving above the group */ + if (indicator == QAbstractItemView::AboveItem && itemIsGroup) + dropGroup = nullptr; + if (emptyDrop) + dropGroup = nullptr; + + /* --------------------------------------- */ + /* remember to remove list items if */ + /* dropping on collapsed group */ + + bool dropOnCollapsed = false; + if (dropGroup) { + obs_data_t *data = obs_sceneitem_get_private_settings(dropGroup); + dropOnCollapsed = obs_data_get_bool(data, "collapsed"); + obs_data_release(data); + } + + if (indicator == QAbstractItemView::BelowItem || + indicator == QAbstractItemView::OnItem) + row++; + + if (row < 0 || row > stm->items.count()) { + QListView::dropEvent(event); + return; + } + + /* --------------------------------------- */ + /* determine if any base group is selected */ + + bool hasGroups = false; + for (int i = 0; i < indices.size(); i++) { + obs_sceneitem_t *item = items[indices[i].row()]; + if (obs_sceneitem_is_group(item)) { + hasGroups = true; + break; + } + } + + /* --------------------------------------- */ + /* if dropping a group, detect if it's */ + /* below another group */ + + obs_sceneitem_t *itemBelow = row == stm->items.count() + ? nullptr + : stm->items[row]; + if (hasGroups) { + if (!itemBelow || + obs_sceneitem_get_group(scene, itemBelow) != dropGroup) { + indicator = QAbstractItemView::BelowItem; + dropGroup = nullptr; + dropOnCollapsed = false; + } + } + + /* --------------------------------------- */ + /* if dropping groups on other groups, */ + /* disregard as invalid drag/drop */ + + if (dropGroup && hasGroups) { + QListView::dropEvent(event); + return; + } + + /* --------------------------------------- */ + /* if selection includes base group items, */ + /* include all group sub-items and treat */ + /* them all as one */ + + if (hasGroups) { + /* remove sub-items if selected */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_scene_t *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + indices.removeAt(i); + } + } + + /* add all sub-items of selected groups */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + + if (obs_sceneitem_is_group(item)) { + for (int j = items.size() - 1; j >= 0; j--) { + obs_sceneitem_t *subitem = items[j]; + obs_sceneitem_t *subitemGroup = + obs_sceneitem_get_group(scene, + subitem); + + if (subitemGroup == item) { + QModelIndex idx = + stm->createIndex(j, 0); + indices.insert(i + 1, idx); + } + } + } + } + } + + /* --------------------------------------- */ + /* build persistent indices */ + + QList persistentIndices; + persistentIndices.reserve(indices.count()); + for (QModelIndex &index : indices) + persistentIndices.append(index); + std::sort(persistentIndices.begin(), persistentIndices.end()); + + /* --------------------------------------- */ + /* move all items to destination index */ + + int r = row; + for (auto &persistentIdx : persistentIndices) { + int from = persistentIdx.row(); + int to = r; + int itemTo = to; + + if (itemTo > from) + itemTo--; + + if (itemTo != from) { + stm->beginMoveRows(QModelIndex(), from, from, + QModelIndex(), to); + MoveItem(items, from, itemTo); + stm->endMoveRows(); + } + + r = persistentIdx.row() + 1; + } + + std::sort(persistentIndices.begin(), persistentIndices.end()); + int firstIdx = persistentIndices.front().row(); + int lastIdx = persistentIndices.back().row(); + + /* --------------------------------------- */ + /* reorder scene items in back-end */ + + QVector orderList; + obs_sceneitem_t *lastGroup = nullptr; + int insertCollapsedIdx = 0; + + auto insertCollapsed = [&] (obs_sceneitem_t *item) + { + struct obs_sceneitem_order_info info; + info.group = lastGroup; + info.item = item; + + orderList.insert(insertCollapsedIdx++, info); + }; + + using insertCollapsed_t = decltype(insertCollapsed); + + auto preInsertCollapsed = [] (obs_scene_t *, obs_sceneitem_t *item, + void *param) + { + (*reinterpret_cast(param))(item); + return true; + }; + + auto insertLastGroup = [&] () + { + obs_data_t *data = obs_sceneitem_get_private_settings(lastGroup); + bool collapsed = obs_data_get_bool(data, "collapsed"); + obs_data_release(data); + + if (collapsed) { + insertCollapsedIdx = 0; + obs_sceneitem_group_enum_items( + lastGroup, + preInsertCollapsed, + &insertCollapsed); + } + + struct obs_sceneitem_order_info info; + info.group = nullptr; + info.item = lastGroup; + orderList.insert(0, info); + }; + + auto updateScene = [&] () + { + struct obs_sceneitem_order_info info; + + for (int i = 0; i < items.size(); i++) { + obs_sceneitem_t *item = items[i]; + obs_sceneitem_t *group; + + if (obs_sceneitem_is_group(item)) { + if (lastGroup) { + insertLastGroup(); + } + lastGroup = item; + continue; + } + + if (!hasGroups && i >= firstIdx && i <= lastIdx) + group = dropGroup; + else + group = obs_sceneitem_get_group(scene, item); + + if (lastGroup && lastGroup != group) { + insertLastGroup(); + } + + lastGroup = group; + + info.group = group; + info.item = item; + orderList.insert(0, info); + } + + if (lastGroup) { + insertLastGroup(); + } + + obs_scene_reorder_items2(scene, + orderList.data(), orderList.size()); + }; + + using updateScene_t = decltype(updateScene); + + auto preUpdateScene = [] (void *data, obs_scene_t *) + { + (*reinterpret_cast(data))(); + }; + + ignoreReorder = true; + obs_scene_atomic_update(scene, preUpdateScene, &updateScene); + ignoreReorder = false; + + /* --------------------------------------- */ + /* remove items if dropped in to collapsed */ + /* group */ + + if (dropOnCollapsed) { + stm->beginRemoveRows(QModelIndex(), firstIdx, lastIdx); + items.remove(firstIdx, lastIdx - firstIdx + 1); + stm->endRemoveRows(); + } + + /* --------------------------------------- */ + /* update widgets and accept event */ + + UpdateWidgets(true); + + event->accept(); + event->setDropAction(Qt::CopyAction); + + QListView::dropEvent(event); +} + +void SourceTree::selectionChanged( + const QItemSelection &selected, + const QItemSelection &deselected) +{ + { + SignalBlocker sourcesSignalBlocker(this); + SourceTreeModel *stm = GetStm(); + + QModelIndexList selectedIdxs = selected.indexes(); + QModelIndexList deselectedIdxs = deselected.indexes(); + + for (int i = 0; i < selectedIdxs.count(); i++) { + int idx = selectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], true); + } + + for (int i = 0; i < deselectedIdxs.count(); i++) { + int idx = deselectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], false); + } + } + QListView::selectionChanged(selected, deselected); +} + +void SourceTree::Edit(int row) +{ + SourceTreeModel *stm = GetStm(); + if (row < 0 || row >= stm->items.count()) + return; + + QWidget *widget = indexWidget(stm->createIndex(row, 0)); + SourceTreeItem *itemWidget = reinterpret_cast(widget); + itemWidget->EnterEditMode(); + edit(stm->createIndex(row, 0)); +} + +bool SourceTree::MultipleBaseSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (obs_sceneitem_is_group(item)) { + return false; + } + + obs_scene *itemScene = obs_sceneitem_get_scene(item); + if (itemScene != scene) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (!obs_sceneitem_is_group(item)) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupedItemsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + OBSScene scene = GetCurrentScene(); + + if (!selectedIndices.size()) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + obs_scene *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + return true; + } + } + + return false; +} + +void SourceTree::Remove(OBSSceneItem item) +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + GetStm()->Remove(item); + main->SaveProject(); + + if (!main->SavingDisabled()) { + obs_scene_t *scene = obs_sceneitem_get_scene(item); + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User Removed source '%s' (%s) from scene '%s'", + obs_source_get_name(itemSource), + obs_source_get_id(itemSource), + obs_source_get_name(sceneSource)); + } +} + +void SourceTree::GroupSelectedItems() +{ + QModelIndexList indices = selectedIndexes(); + std::sort(indices.begin(), indices.end()); + GetStm()->GroupSelectedItems(indices); +} + +void SourceTree::UngroupSelectedGroups() +{ + QModelIndexList indices = selectedIndexes(); + GetStm()->UngroupSelectedGroups(indices); +} + +void SourceTree::AddGroup() +{ + GetStm()->AddGroup(); +} diff --git a/UI/source-tree.hpp b/UI/source-tree.hpp new file mode 100644 index 0000000..f77ae90 --- /dev/null +++ b/UI/source-tree.hpp @@ -0,0 +1,177 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class QLabel; +class QCheckBox; +class QLineEdit; +class SourceTree; +class QSpacerItem; +class QHBoxLayout; +class LockedCheckBox; +class VisibilityCheckBox; +class VisibilityItemWidget; + +class SourceTreeSubItemCheckBox : public QCheckBox { + Q_OBJECT +}; + +class SourceTreeItem : public QWidget { + Q_OBJECT + + friend class SourceTree; + friend class SourceTreeModel; + + void mouseDoubleClickEvent(QMouseEvent *event) override; + + virtual bool eventFilter(QObject *object, QEvent *event) override; + + void Update(bool force); + + enum class Type { + Unknown, + Item, + Group, + SubItem, + }; + + void DisconnectSignals(); + void ReconnectSignals(); + + Type type = Type::Unknown; + +public: + explicit SourceTreeItem(SourceTree *tree, OBSSceneItem sceneitem); + +private: + QSpacerItem *spacer = nullptr; + QCheckBox *expand = nullptr; + VisibilityCheckBox *vis = nullptr; + LockedCheckBox *lock = nullptr; + QHBoxLayout *boxLayout = nullptr; + QLabel *label = nullptr; + + QLineEdit *editor = nullptr; + + SourceTree *tree; + OBSSceneItem sceneitem; + OBSSignal sceneRemoveSignal; + OBSSignal itemRemoveSignal; + OBSSignal groupReorderSignal; + OBSSignal deselectSignal; + OBSSignal visibleSignal; + OBSSignal renameSignal; + OBSSignal removeSignal; + + virtual void paintEvent(QPaintEvent* event) override; + +private slots: + void EnterEditMode(); + void ExitEditMode(bool save); + + void VisibilityChanged(bool visible); + void Renamed(const QString &name); + + void ExpandClicked(bool checked); + + void Deselect(); +}; + +class SourceTreeModel : public QAbstractListModel { + Q_OBJECT + + friend class SourceTree; + friend class SourceTreeItem; + + SourceTree *st; + QVector items; + bool hasGroups = false; + + static void OBSFrontendEvent(enum obs_frontend_event event, void *ptr); + void Clear(); + void SceneChanged(); + void ReorderItems(); + + void Add(obs_sceneitem_t *item); + void Remove(obs_sceneitem_t *item); + OBSSceneItem Get(int idx); + QString GetNewGroupName(); + void AddGroup(); + + void GroupSelectedItems(QModelIndexList &indices); + void UngroupSelectedGroups(QModelIndexList &indices); + + void ExpandGroup(obs_sceneitem_t *item); + void CollapseGroup(obs_sceneitem_t *item); + + void UpdateGroupState(bool update); + +public: + explicit SourceTreeModel(SourceTree *st); + ~SourceTreeModel(); + + virtual int rowCount(const QModelIndex &parent) const override; + virtual QVariant data(const QModelIndex &index, int role) const override; + + virtual Qt::ItemFlags flags(const QModelIndex &index) const override; + virtual Qt::DropActions supportedDropActions() const override; +}; + +class SourceTree : public QListView { + Q_OBJECT + + bool ignoreReorder = false; + + friend class SourceTreeModel; + friend class SourceTreeItem; + + void ResetWidgets(); + void UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item); + void UpdateWidgets(bool force = false); + + inline SourceTreeModel *GetStm() const + { + return reinterpret_cast(model()); + } + +public: + inline SourceTreeItem *GetItemWidget(int idx) + { + QWidget *widget = indexWidget(GetStm()->createIndex(idx, 0)); + return reinterpret_cast(widget); + } + + explicit SourceTree(QWidget *parent = nullptr); + + inline bool IgnoreReorder() const {return ignoreReorder;} + inline void Clear() {GetStm()->Clear();} + + inline void Add(obs_sceneitem_t *item) {GetStm()->Add(item);} + inline OBSSceneItem Get(int idx) {return GetStm()->Get(idx);} + inline QString GetNewGroupName() {return GetStm()->GetNewGroupName();} + + void SelectItem(obs_sceneitem_t *sceneitem, bool select); + + bool MultipleBaseSelected() const; + bool GroupsSelected() const; + bool GroupedItemsSelected() const; + +public slots: + inline void ReorderItems() {GetStm()->ReorderItems();} + void Remove(OBSSceneItem item); + void GroupSelectedItems(); + void UngroupSelectedGroups(); + void AddGroup(); + void Edit(int idx); + +protected: + virtual void mouseDoubleClickEvent(QMouseEvent *event) override; + virtual void dropEvent(QDropEvent *event) override; + + virtual void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override; +}; diff --git a/UI/volume-control.cpp b/UI/volume-control.cpp index 47019cc..80fbe1f 100644 --- a/UI/volume-control.cpp +++ b/UI/volume-control.cpp @@ -3,24 +3,17 @@ #include "obs-app.hpp" #include "mute-checkbox.hpp" #include "slider-absoluteset-style.hpp" -#include -#include -#include #include #include -#include #include -#include #include #include #include -#include -#include -#include +#include using namespace std; -#define CLAMP(x, min, max) ((x) < min ? min : ((x) > max ? max : (x))) +#define CLAMP(x, min, max) ((x) < (min) ? (min) : ((x) > (max) ? (max) : (x))) QWeakPointer VolumeMeter::updateTimer; @@ -33,9 +26,9 @@ void VolControl::OBSVolumeChanged(void *data, float db) } void VolControl::OBSVolumeLevel(void *data, - const float magnitude[MAX_AUDIO_CHANNELS], - const float peak[MAX_AUDIO_CHANNELS], - const float inputPeak[MAX_AUDIO_CHANNELS]) + const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]) { VolControl *volControl = static_cast(data); @@ -114,55 +107,24 @@ void VolControl::SetMeterDecayRate(qreal q) volMeter->setPeakDecayRate(q); } -VolControl::VolControl(OBSSource source_, bool showConfig) - : source (source_), - levelTotal (0.0f), - levelCount (0.0f), - obs_fader (obs_fader_create(OBS_FADER_CUBIC)), - obs_volmeter (obs_volmeter_create(OBS_FADER_LOG)) +void VolControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType) { - QHBoxLayout *volLayout = new QHBoxLayout(); - QVBoxLayout *mainLayout = new QVBoxLayout(); - QHBoxLayout *textLayout = new QHBoxLayout(); - QHBoxLayout *botLayout = new QHBoxLayout(); + volMeter->setPeakMeterType(peakMeterType); +} +VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical) + : source (std::move(source_)), + levelTotal (0.0f), + levelCount (0.0f), + obs_fader (obs_fader_create(OBS_FADER_CUBIC)), + obs_volmeter (obs_volmeter_create(OBS_FADER_LOG)), + vertical (vertical) +{ nameLabel = new QLabel(); volLabel = new QLabel(); - volMeter = new VolumeMeter(0, obs_volmeter); mute = new MuteCheckBox(); - slider = new QSlider(Qt::Horizontal); - - QFont font = nameLabel->font(); - font.setPointSize(font.pointSize()-1); - QString sourceName = obs_source_get_name(source); - - nameLabel->setText(sourceName); - nameLabel->setFont(font); - volLabel->setFont(font); - slider->setMinimum(0); - slider->setMaximum(100); - -// slider->setMaximumHeight(13); - - textLayout->setContentsMargins(0, 0, 0, 0); - textLayout->addWidget(nameLabel); - textLayout->addWidget(volLabel); - textLayout->setAlignment(nameLabel, Qt::AlignLeft); - textLayout->setAlignment(volLabel, Qt::AlignRight); - - bool muted = obs_source_muted(source); - mute->setChecked(muted); - mute->setAccessibleName( - QTStr("VolControl.Mute").arg(sourceName)); - - volLayout->addWidget(slider); - volLayout->addWidget(mute); - volLayout->setSpacing(5); - - botLayout->setContentsMargins(0, 0, 0, 0); - botLayout->setSpacing(0); - botLayout->addLayout(volLayout); + setObjectName(sourceName); if (showConfig) { config = new QPushButton(this); @@ -178,18 +140,99 @@ VolControl::VolControl(OBSSource source_, bool showConfig) connect(config, &QAbstractButton::clicked, this, &VolControl::EmitConfigClicked); - - botLayout->addWidget(config); } + QVBoxLayout *mainLayout = new QVBoxLayout; mainLayout->setContentsMargins(4, 4, 4, 4); mainLayout->setSpacing(2); - mainLayout->addItem(textLayout); - mainLayout->addWidget(volMeter); - mainLayout->addItem(botLayout); + + if (vertical) { + QHBoxLayout *nameLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QHBoxLayout *volLayout = new QHBoxLayout; + QHBoxLayout *meterLayout = new QHBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, true); + slider = new QSlider(Qt::Vertical); + + nameLayout->setAlignment(Qt::AlignCenter); + meterLayout->setAlignment(Qt::AlignCenter); + controlLayout->setAlignment(Qt::AlignCenter); + volLayout->setAlignment(Qt::AlignCenter); + + nameLayout->setContentsMargins(0, 0, 0, 0); + nameLayout->setSpacing(0); + nameLayout->addWidget(nameLabel); + + controlLayout->setContentsMargins(0, 0, 0, 0); + controlLayout->setSpacing(0); + + if (showConfig) + controlLayout->addWidget(config); + + controlLayout->addItem(new QSpacerItem(3, 0)); + // Add Headphone (audio monitoring) widget here + controlLayout->addWidget(mute); + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + meterLayout->addWidget(volMeter); + meterLayout->addWidget(slider); + + volLayout->setContentsMargins(0, 0, 0, 0); + volLayout->setSpacing(0); + volLayout->addWidget(volLabel); + + mainLayout->addItem(nameLayout); + mainLayout->addItem(volLayout); + mainLayout->addItem(meterLayout); + mainLayout->addItem(controlLayout); + + setMaximumWidth(110); + } else { + QHBoxLayout *volLayout = new QHBoxLayout; + QHBoxLayout *textLayout = new QHBoxLayout; + QHBoxLayout *botLayout = new QHBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, false); + slider = new QSlider(Qt::Horizontal); + + textLayout->setContentsMargins(0, 0, 0, 0); + textLayout->addWidget(nameLabel); + textLayout->addWidget(volLabel); + textLayout->setAlignment(nameLabel, Qt::AlignLeft); + textLayout->setAlignment(volLabel, Qt::AlignRight); + + volLayout->addWidget(slider); + volLayout->addWidget(mute); + volLayout->setSpacing(5); + + botLayout->setContentsMargins(0, 0, 0, 0); + botLayout->setSpacing(0); + botLayout->addLayout(volLayout); + + if (showConfig) + botLayout->addWidget(config); + + mainLayout->addItem(textLayout); + mainLayout->addWidget(volMeter); + mainLayout->addItem(botLayout); + } setLayout(mainLayout); + QFont font = nameLabel->font(); + font.setPointSize(font.pointSize()-1); + + nameLabel->setText(sourceName); + nameLabel->setFont(font); + volLabel->setFont(font); + slider->setMinimum(0); + slider->setMaximum(100); + + bool muted = obs_source_muted(source); + mute->setChecked(muted); + mute->setAccessibleName(QTStr("VolControl.Mute").arg(sourceName)); obs_fader_add_callback(obs_fader, OBSVolumeChanged, this); obs_volmeter_add_callback(obs_volmeter, OBSVolumeLevel, this); @@ -204,7 +247,17 @@ VolControl::VolControl(OBSSource source_, bool showConfig) obs_fader_attach_source(obs_fader, source); obs_volmeter_attach_source(obs_volmeter, source); - slider->setStyle(new SliderAbsoluteSetStyle(slider->style())); + QString styleName = slider->style()->objectName(); + QStyle *style; + style = QStyleFactory::create(styleName); + if (!style) { + style = new SliderAbsoluteSetStyle(); + } else { + style = new SliderAbsoluteSetStyle(style); + } + + style->setParent(slider); + slider->setStyle(style); /* Call volume changed once to init the slider position and label */ VolumeChanged(); @@ -229,7 +282,7 @@ QColor VolumeMeter::getBackgroundNominalColor() const void VolumeMeter::setBackgroundNominalColor(QColor c) { - backgroundNominalColor = c; + backgroundNominalColor = std::move(c); } QColor VolumeMeter::getBackgroundWarningColor() const @@ -239,7 +292,7 @@ QColor VolumeMeter::getBackgroundWarningColor() const void VolumeMeter::setBackgroundWarningColor(QColor c) { - backgroundWarningColor = c; + backgroundWarningColor = std::move(c); } QColor VolumeMeter::getBackgroundErrorColor() const @@ -249,7 +302,7 @@ QColor VolumeMeter::getBackgroundErrorColor() const void VolumeMeter::setBackgroundErrorColor(QColor c) { - backgroundErrorColor = c; + backgroundErrorColor = std::move(c); } QColor VolumeMeter::getForegroundNominalColor() const @@ -259,7 +312,7 @@ QColor VolumeMeter::getForegroundNominalColor() const void VolumeMeter::setForegroundNominalColor(QColor c) { - foregroundNominalColor = c; + foregroundNominalColor = std::move(c); } QColor VolumeMeter::getForegroundWarningColor() const @@ -269,7 +322,7 @@ QColor VolumeMeter::getForegroundWarningColor() const void VolumeMeter::setForegroundWarningColor(QColor c) { - foregroundWarningColor = c; + foregroundWarningColor = std::move(c); } QColor VolumeMeter::getForegroundErrorColor() const @@ -279,7 +332,7 @@ QColor VolumeMeter::getForegroundErrorColor() const void VolumeMeter::setForegroundErrorColor(QColor c) { - foregroundErrorColor = c; + foregroundErrorColor = std::move(c); } QColor VolumeMeter::getClipColor() const @@ -289,7 +342,7 @@ QColor VolumeMeter::getClipColor() const void VolumeMeter::setClipColor(QColor c) { - clipColor = c; + clipColor = std::move(c); } QColor VolumeMeter::getMagnitudeColor() const @@ -299,7 +352,7 @@ QColor VolumeMeter::getMagnitudeColor() const void VolumeMeter::setMagnitudeColor(QColor c) { - magnitudeColor = c; + magnitudeColor = std::move(c); } QColor VolumeMeter::getMajorTickColor() const @@ -309,7 +362,7 @@ QColor VolumeMeter::getMajorTickColor() const void VolumeMeter::setMajorTickColor(QColor c) { - majorTickColor = c; + majorTickColor = std::move(c); } QColor VolumeMeter::getMinorTickColor() const @@ -319,7 +372,7 @@ QColor VolumeMeter::getMinorTickColor() const void VolumeMeter::setMinorTickColor(QColor c) { - minorTickColor = c; + minorTickColor = std::move(c); } qreal VolumeMeter::getMinimumLevel() const @@ -412,8 +465,43 @@ void VolumeMeter::setInputPeakHoldDuration(qreal v) inputPeakHoldDuration = v; } -VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter) - : QWidget(parent), obs_volmeter(obs_volmeter) +void VolumeMeter::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + obs_volmeter_set_peak_meter_type(obs_volmeter, peakMeterType); + switch (peakMeterType) { + case TRUE_PEAK_METER: + // For true-peak meters EBU has defined the Permitted Maximum, + // taking into account the accuracy of the meter and further + // processing required by lossy audio compression. + // + // The alignment level was not specified, but I've adjusted + // it compared to a sample-peak meter. Incidently Youtube + // uses this new Alignment Level as the maximum integrated + // loudness of a video. + // + // * Permitted Maximum Level (PML) = -2.0 dBTP + // * Alignment Level (AL) = -13 dBTP + setErrorLevel(-2.0); + setWarningLevel(-13.0); + break; + + case SAMPLE_PEAK_METER: + default: + // For a sample Peak Meter EBU has the following level + // definitions, taking into account inaccuracies of this meter: + // + // * Permitted Maximum Level (PML) = -9.0 dBFS + // * Alignment Level (AL) = -20.0 dBFS + setErrorLevel(-9.0); + setWarningLevel(-20.0); + break; + } +} + +VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter, + bool vertical) + : QWidget(parent), obs_volmeter(obs_volmeter), + vertical(vertical) { // Use a font that can be rendered small. tickFont = QFont("Arial"); @@ -454,12 +542,12 @@ VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter) VolumeMeter::~VolumeMeter() { updateTimerRef->RemoveVolControl(this); + delete tickPaintCache; } -void VolumeMeter::setLevels( - const float magnitude[MAX_AUDIO_CHANNELS], - const float peak[MAX_AUDIO_CHANNELS], - const float inputPeak[MAX_AUDIO_CHANNELS]) +void VolumeMeter::setLevels(const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]) { uint64_t ts = os_gettime_ns(); QMutexLocker locker(&dataMutex); @@ -502,9 +590,12 @@ inline void VolumeMeter::handleChannelCofigurationChange() if (displayNrAudioChannels != currentNrAudioChannels) { displayNrAudioChannels = currentNrAudioChannels; - // Make room for 3 pixels high meter, with one pixel between - // each. Then 9 pixels below it for ticks and numbers. - setMinimumSize(130, displayNrAudioChannels * 4 + 8); + // Make room for 3 pixels meter, with one pixel between each. + // Then 9/13 pixels for ticks and numbers. + if (vertical) + setMinimumSize(displayNrAudioChannels * 4 + 14, 130); + else + setMinimumSize(130, displayNrAudioChannels * 4 + 8); resetLevels(); } @@ -512,7 +603,7 @@ inline void VolumeMeter::handleChannelCofigurationChange() inline bool VolumeMeter::detectIdle(uint64_t ts) { - float timeSinceLastUpdate = (ts - currentLastUpdateTime) * 0.000000001; + double timeSinceLastUpdate = (ts - currentLastUpdateTime) * 0.000000001; if (timeSinceLastUpdate > 0.5) { resetLevels(); return true; @@ -522,7 +613,7 @@ inline bool VolumeMeter::detectIdle(uint64_t ts) } inline void VolumeMeter::calculateBallisticsForChannel(int channelNr, - uint64_t ts, qreal timeSinceLastRedraw) + uint64_t ts, qreal timeSinceLastRedraw) { if (currentPeak[channelNr] >= displayPeak[channelNr] || isnan(displayPeak[channelNr])) { @@ -532,7 +623,7 @@ inline void VolumeMeter::calculateBallisticsForChannel(int channelNr, // Decay of peak is 40 dB / 1.7 seconds for Fast Profile // 20 dB / 1.7 seconds for Medium Profile (Type I PPM) // 24 dB / 2.8 seconds for Slow Profile (Type II PPM) - qreal decay = peakDecayRate * timeSinceLastRedraw; + float decay = float(peakDecayRate * timeSinceLastRedraw); displayPeak[channelNr] = CLAMP(displayPeak[channelNr] - decay, currentPeak[channelNr], 0); } @@ -576,53 +667,52 @@ inline void VolumeMeter::calculateBallisticsForChannel(int channelNr, if (!isfinite(displayMagnitude[channelNr])) { // The statements in the else-leg do not work with // NaN and infinite displayMagnitude. - displayMagnitude[channelNr] = - currentMagnitude[channelNr]; + displayMagnitude[channelNr] = currentMagnitude[channelNr]; } else { // A VU meter will integrate to the new value to 99% in 300 ms. // The calculation here is very simplified and is more accurate // with higher frame-rate. - qreal attack = (currentMagnitude[channelNr] - + float attack = float((currentMagnitude[channelNr] - displayMagnitude[channelNr]) * (timeSinceLastRedraw / - magnitudeIntegrationTime) * 0.99; - displayMagnitude[channelNr] = CLAMP( - displayMagnitude[channelNr] + attack, - minimumLevel, 0); + magnitudeIntegrationTime) * 0.99); + displayMagnitude[channelNr] = CLAMP(displayMagnitude[channelNr] + + attack, (float)minimumLevel, 0); } } inline void VolumeMeter::calculateBallistics(uint64_t ts, - qreal timeSinceLastRedraw) + qreal timeSinceLastRedraw) { QMutexLocker locker(&dataMutex); - for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) calculateBallisticsForChannel(channelNr, ts, - timeSinceLastRedraw); - } + timeSinceLastRedraw); } -void VolumeMeter::paintInputMeter(QPainter &painter, int x, int y, - int width, int height, float peakHold) +void VolumeMeter::paintInputMeter(QPainter &painter, int x, int y, int width, + int height, float peakHold) { QMutexLocker locker(&dataMutex); + QColor color; - if (peakHold < minimumInputLevel) { - painter.fillRect(x, y, width, height, backgroundNominalColor); - } else if (peakHold < warningLevel) { - painter.fillRect(x, y, width, height, foregroundNominalColor); - } else if (peakHold < errorLevel) { - painter.fillRect(x, y, width, height, foregroundWarningColor); - } else if (peakHold <= clipLevel) { - painter.fillRect(x, y, width, height, foregroundErrorColor); - } else { - painter.fillRect(x, y, width, height, clipColor); - } + if (peakHold < minimumInputLevel) + color = backgroundNominalColor; + else if (peakHold < warningLevel) + color = foregroundNominalColor; + else if (peakHold < errorLevel) + color = foregroundWarningColor; + else if (peakHold <= clipLevel) + color = foregroundErrorColor; + else + color = clipColor; + + painter.fillRect(x, y, width, height, color); } -void VolumeMeter::paintTicks(QPainter &painter, int x, int y, - int width, int height) +void VolumeMeter::paintHTicks(QPainter &painter, int x, int y, int width, + int height) { qreal scale = width / minimumLevel; @@ -631,184 +721,257 @@ void VolumeMeter::paintTicks(QPainter &painter, int x, int y, // Draw major tick lines and numeric indicators. for (int i = 0; i >= minimumLevel; i-= 5) { - int position = x + width - (i * scale) - 1; + int position = int(x + width - (i * scale) - 1); QString str = QString::number(i); - if (i == 0 || i == -5) { + if (i == 0 || i == -5) painter.drawText(position - 3, height, str); - } else { + else painter.drawText(position - 5, height, str); - } painter.drawLine(position, y, position, y + 2); } // Draw minor tick lines. painter.setPen(minorTickColor); for (int i = 0; i >= minimumLevel; i--) { - int position = x + width - (i * scale) - 1; - - if (i % 5 != 0) { + int position = int(x + width - (i * scale) - 1); + if (i % 5 != 0) painter.drawLine(position, y, position, y + 1); - } } } -void VolumeMeter::paintMeter(QPainter &painter, int x, int y, - int width, int height, float magnitude, float peak, float peakHold) +void VolumeMeter::paintVTicks(QPainter &painter, int x, int y, int height) +{ + qreal scale = height / minimumLevel; + + painter.setFont(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i-= 5) { + int position = y + int((i * scale) - 1); + QString str = QString::number(i); + + if (i == 0) + painter.drawText(x + 5, position + 4, str); + else if (i == -60) + painter.drawText(x + 4, position, str); + else + painter.drawText(x + 4, position + 2, str); + painter.drawLine(x, position, x + 2, position); + } + + // Draw minor tick lines. + painter.setPen(minorTickColor); + for (int i = 0; i >= minimumLevel; i--) { + int position = y + int((i * scale) - 1); + if (i % 5 != 0) + painter.drawLine(x, position, x + 1, position); + } +} + +#define CLIP_FLASH_DURATION_MS 1000 + +void VolumeMeter::ClipEnding() +{ + clipping = false; +} + +void VolumeMeter::paintHMeter(QPainter &painter, int x, int y, int width, + int height, float magnitude, float peak, float peakHold) { qreal scale = width / minimumLevel; QMutexLocker locker(&dataMutex); int minimumPosition = x + 0; int maximumPosition = x + width; - int magnitudePosition = x + width - (magnitude * scale); - int peakPosition = x + width - (peak * scale); - int peakHoldPosition = x + width - (peakHold * scale); - int warningPosition = x + width - (warningLevel * scale); - int errorPosition = x + width - (errorLevel * scale); + int magnitudePosition = int(x + width - (magnitude * scale)); + int peakPosition = int(x + width - (peak * scale)); + int peakHoldPosition = int(x + width - (peakHold * scale)); + int warningPosition = int(x + width - (warningLevel * scale)); + int errorPosition = int(x + width - (errorLevel * scale)); int nominalLength = warningPosition - minimumPosition; int warningLength = errorPosition - warningPosition; int errorLength = maximumPosition - errorPosition; locker.unlock(); + if (clipping) { + peakPosition = maximumPosition; + } + if (peakPosition < minimumPosition) { - painter.fillRect( - minimumPosition, y, - nominalLength, height, - backgroundNominalColor); - painter.fillRect( - warningPosition, y, - warningLength, height, - backgroundWarningColor); - painter.fillRect( - errorPosition, y, - errorLength, height, - backgroundErrorColor); - + painter.fillRect(minimumPosition, y, nominalLength, height, + backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + backgroundErrorColor); } else if (peakPosition < warningPosition) { - painter.fillRect( - minimumPosition, y, - peakPosition - minimumPosition, height, - foregroundNominalColor); - painter.fillRect( - peakPosition, y, - warningPosition - peakPosition, height, - backgroundNominalColor); - painter.fillRect( - warningPosition, y, - warningLength, height, - backgroundWarningColor); - painter.fillRect(errorPosition, y, - errorLength, height, - backgroundErrorColor); - + painter.fillRect(minimumPosition, y, peakPosition - + minimumPosition, height, + foregroundNominalColor); + painter.fillRect(peakPosition, y, warningPosition - + peakPosition, height, backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + backgroundErrorColor); } else if (peakPosition < errorPosition) { - painter.fillRect( - minimumPosition, y, - nominalLength, height, - foregroundNominalColor); - painter.fillRect( - warningPosition, y, - peakPosition - warningPosition, height, - foregroundWarningColor); - painter.fillRect( - peakPosition, y, - errorPosition - peakPosition, height, - backgroundWarningColor); - painter.fillRect( - errorPosition, y, - errorLength, height, - backgroundErrorColor); - + painter.fillRect(minimumPosition, y, nominalLength, height, + foregroundNominalColor); + painter.fillRect(warningPosition, y, + peakPosition - warningPosition, height, + foregroundWarningColor); + painter.fillRect(peakPosition, y, errorPosition - + peakPosition, height, backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + backgroundErrorColor); } else if (peakPosition < maximumPosition) { - painter.fillRect( - minimumPosition, y, - nominalLength, height, - foregroundNominalColor); - painter.fillRect( - warningPosition, y, - warningLength, height, - foregroundWarningColor); - painter.fillRect( - errorPosition, y, - peakPosition - errorPosition, height, - foregroundErrorColor); - painter.fillRect( - peakPosition, y, - maximumPosition - peakPosition, height, - backgroundErrorColor); - + painter.fillRect(minimumPosition, y, nominalLength, height, + foregroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + foregroundWarningColor); + painter.fillRect(errorPosition, y, peakPosition - errorPosition, + height, foregroundErrorColor); + painter.fillRect(peakPosition, y, + maximumPosition - peakPosition, height, + backgroundErrorColor); } else { - qreal end = errorLength + warningLength + nominalLength; - painter.fillRect( - minimumPosition, y, - end, height, - QBrush(foregroundErrorColor)); + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, + SLOT(ClipEnding())); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(minimumPosition, y, end, height, + QBrush(foregroundErrorColor)); } - if (peakHoldPosition - 3 < minimumPosition) { - // Peak-hold below minimum, no drawing. + if (peakHoldPosition - 3 < minimumPosition) + ;// Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + foregroundWarningColor); + else + painter.fillRect(peakHoldPosition - 3, y, 3, height, + foregroundErrorColor); - } else if (peakHoldPosition < warningPosition) { - painter.fillRect( - peakHoldPosition - 3, y, - 3, height, - foregroundNominalColor); + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(magnitudePosition - 3, y, 3, height, + magnitudeColor); +} - } else if (peakHoldPosition < errorPosition) { - painter.fillRect( - peakHoldPosition - 3, y, - 3, height, - foregroundWarningColor); +void VolumeMeter::paintVMeter(QPainter &painter, int x, int y, int width, + int height, float magnitude, float peak, float peakHold) +{ + qreal scale = height / minimumLevel; - } else { - painter.fillRect( - peakHoldPosition - 3, y, - 3, height, - foregroundErrorColor); + QMutexLocker locker(&dataMutex); + int minimumPosition = y + 0; + int maximumPosition = y + height; + int magnitudePosition = int(y + height - (magnitude * scale)); + int peakPosition = int(y + height - (peak * scale)); + int peakHoldPosition = int(y + height - (peakHold * scale)); + int warningPosition = int(y + height - (warningLevel * scale)); + int errorPosition = int(y + height - (errorLevel * scale)); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; } - if (magnitudePosition - 3 < minimumPosition) { - // Magnitude below minimum, no drawing. - - } else if (magnitudePosition < warningPosition) { - painter.fillRect( - magnitudePosition - 3, y, - 3, height, - magnitudeColor); - - } else if (magnitudePosition < errorPosition) { - painter.fillRect( - magnitudePosition - 3, y, - 3, height, - magnitudeColor); - + if (peakPosition < minimumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(x, minimumPosition, width, peakPosition - + minimumPosition, foregroundNominalColor); + painter.fillRect(x, peakPosition, width, warningPosition - + peakPosition, backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(x,minimumPosition, width, nominalLength, + foregroundNominalColor); + painter.fillRect(x, warningPosition, width, peakPosition - + warningPosition, foregroundWarningColor); + painter.fillRect(x, peakPosition, width, errorPosition - + peakPosition, backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + foregroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + foregroundWarningColor); + painter.fillRect(x, errorPosition, width, peakPosition - + errorPosition, foregroundErrorColor); + painter.fillRect(x, peakPosition, width, maximumPosition - + peakPosition, backgroundErrorColor); } else { - painter.fillRect( - magnitudePosition - 3, y, - 3, height, - magnitudeColor); + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, + SLOT(ClipEnding())); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(x, minimumPosition, width, end, + QBrush(foregroundErrorColor)); } + + if (peakHoldPosition - 3 < minimumPosition) + ;// Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + foregroundWarningColor); + else + painter.fillRect(x, peakHoldPosition - 3, width, 3, + foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(x, magnitudePosition - 3, width, 3, + magnitudeColor); } void VolumeMeter::paintEvent(QPaintEvent *event) { - UNUSED_PARAMETER(event); - uint64_t ts = os_gettime_ns(); qreal timeSinceLastRedraw = (ts - lastRedrawTime) * 0.000000001; - int width = size().width(); - int height = size().height(); + const QRect rect = event->region().boundingRect(); + int width = rect.width(); + int height = rect.height(); handleChannelCofigurationChange(); calculateBallistics(ts, timeSinceLastRedraw); bool idle = detectIdle(ts); // Draw the ticks in a off-screen buffer when the widget changes size. - QSize tickPaintCacheSize = QSize(width, 9); - if (tickPaintCache == NULL || + QSize tickPaintCacheSize; + if (vertical) + tickPaintCacheSize = QSize(14, height); + else + tickPaintCacheSize = QSize(width, 9); + if (tickPaintCache == nullptr || tickPaintCache->size() != tickPaintCacheSize) { delete tickPaintCache; tickPaintCache = new QPixmap(tickPaintCacheSize); @@ -817,30 +980,56 @@ void VolumeMeter::paintEvent(QPaintEvent *event) tickPaintCache->fill(clearColor); QPainter tickPainter(tickPaintCache); - paintTicks(tickPainter, 6, 0, tickPaintCacheSize.width() - 6, - tickPaintCacheSize.height()); + if (vertical) { + tickPainter.translate(0, height); + tickPainter.scale(1, -1); + paintVTicks(tickPainter, 0, 11, + tickPaintCacheSize.height() - 11); + } else { + paintHTicks(tickPainter, 6, 0, + tickPaintCacheSize.width() - 6, + tickPaintCacheSize.height()); + } tickPainter.end(); } // Actual painting of the widget starts here. QPainter painter(this); - painter.drawPixmap(0, height - 9, *tickPaintCache); + if (vertical) { + // Invert the Y axis to ease the math + painter.translate(0, height); + painter.scale(1, -1); + painter.drawPixmap(displayNrAudioChannels * 4 - 1, 7, + *tickPaintCache); + } else { + painter.drawPixmap(0, height - 9, *tickPaintCache); + } for (int channelNr = 0; channelNr < displayNrAudioChannels; channelNr++) { - paintMeter(painter, - 5, channelNr * 4, width - 5, 3, - displayMagnitude[channelNr], displayPeak[channelNr], - displayPeakHold[channelNr]); + if (vertical) + paintVMeter(painter, channelNr * 4, 8, 3, height - 10, + displayMagnitude[channelNr], + displayPeak[channelNr], + displayPeakHold[channelNr]); + else + paintHMeter(painter, 5, channelNr * 4, width - 5, 3, + displayMagnitude[channelNr], + displayPeak[channelNr], + displayPeakHold[channelNr]); - if (!idle) { - // By not drawing the input meter boxes the user can - // see that the audio stream has been stopped, without - // having too much visual impact. - paintInputMeter(painter, - 0, channelNr * 4, 3, 3, - displayInputPeakHold[channelNr]); - } + if (idle) + continue; + + // By not drawing the input meter boxes the user can + // see that the audio stream has been stopped, without + // having too much visual impact. + if (vertical) + paintInputMeter(painter, channelNr * 4, 3, 3, 3, + displayInputPeakHold[channelNr]); + else + paintInputMeter(painter, 0, channelNr * 4, 3, 3, + displayInputPeakHold[channelNr]); } lastRedrawTime = ts; diff --git a/UI/volume-control.hpp b/UI/volume-control.hpp index 390e684..fde0d12 100644 --- a/UI/volume-control.hpp +++ b/UI/volume-control.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -79,6 +80,9 @@ class VolumeMeter : public QWidget READ getInputPeakHoldDuration WRITE setInputPeakHoldDuration DESIGNABLE true) +private slots: + void ClipEnding(); + private: obs_volmeter_t *obs_volmeter; static QWeakPointer updateTimer; @@ -92,12 +96,15 @@ private: inline void calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw); - void paintInputMeter(QPainter &painter, int x, int y, - int width, int height, float peakHold); - void paintMeter(QPainter &painter, int x, int y, - int width, int height, - float magnitude, float peak, float peakHold); - void paintTicks(QPainter &painter, int x, int y, int width, int height); + void paintInputMeter(QPainter &painter, int x, int y, int width, + int height, float peakHold); + void paintHMeter(QPainter &painter, int x, int y, int width, int height, + float magnitude, float peak, float peakHold); + void paintHTicks(QPainter &painter, int x, int y, int width, + int height); + void paintVMeter(QPainter &painter, int x, int y, int width, int height, + float magnitude, float peak, float peakHold); + void paintVTicks(QPainter &painter, int x, int y, int height); QMutex dataMutex; @@ -106,7 +113,7 @@ private: float currentPeak[MAX_AUDIO_CHANNELS]; float currentInputPeak[MAX_AUDIO_CHANNELS]; - QPixmap *tickPaintCache = NULL; + QPixmap *tickPaintCache = nullptr; int displayNrAudioChannels = 0; float displayMagnitude[MAX_AUDIO_CHANNELS]; float displayPeak[MAX_AUDIO_CHANNELS]; @@ -137,10 +144,13 @@ private: qreal inputPeakHoldDuration; uint64_t lastRedrawTime = 0; + bool clipping = false; + bool vertical; public: - explicit VolumeMeter(QWidget *parent = 0, - obs_volmeter_t *obs_volmeter = 0); + explicit VolumeMeter(QWidget *parent = nullptr, + obs_volmeter_t *obs_volmeter = nullptr, + bool vertical = false); ~VolumeMeter(); void setLevels( @@ -186,9 +196,10 @@ public: void setPeakHoldDuration(qreal v); qreal getInputPeakHoldDuration() const; void setInputPeakHoldDuration(qreal v); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); protected: - void paintEvent(QPaintEvent *event); + void paintEvent(QPaintEvent *event) override; }; class VolumeMeterTimer : public QTimer { @@ -201,7 +212,7 @@ public: void RemoveVolControl(VolumeMeter *meter); protected: - virtual void timerEvent(QTimerEvent *event) override; + void timerEvent(QTimerEvent *event) override; QList volumeMeters; }; @@ -224,6 +235,7 @@ private: float levelCount; obs_fader_t *obs_fader; obs_volmeter_t *obs_volmeter; + bool vertical; static void OBSVolumeChanged(void *param, float db); static void OBSVolumeLevel(void *data, @@ -246,7 +258,8 @@ signals: void ConfigClicked(); public: - VolControl(OBSSource source, bool showConfig = false); + explicit VolControl(OBSSource source, bool showConfig = false, + bool vertical = false); ~VolControl(); inline obs_source_t *GetSource() const {return source;} @@ -255,4 +268,5 @@ public: void SetName(const QString &newName); void SetMeterDecayRate(qreal q); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); }; diff --git a/UI/win-update/updater/updater.cpp b/UI/win-update/updater/updater.cpp index 41c34ab..f3735cb 100644 --- a/UI/win-update/updater/updater.cpp +++ b/UI/win-update/updater/updater.cpp @@ -324,6 +324,8 @@ struct update_t { memcpy(hash, from.hash, sizeof(hash)); memcpy(downloadhash, from.downloadhash, sizeof(downloadhash)); memcpy(my_hash, from.my_hash, sizeof(my_hash)); + + return *this; } }; @@ -356,11 +358,11 @@ bool DownloadWorkerThread() WinHttpSetOption(hSession, WINHTTP_OPTION_SECURE_PROTOCOLS, (LPVOID)&tlsProtocols, sizeof(tlsProtocols)); - HttpHandle hConnect = WinHttpConnect(hSession, L"obsproject.com", + HttpHandle hConnect = WinHttpConnect(hSession, L"cdn-fastly.obsproject.com", INTERNET_DEFAULT_HTTPS_PORT, 0); if (!hConnect) { downloadThreadFailure = true; - Status(L"Update failed: Couldn't connect to obsproject.com"); + Status(L"Update failed: Couldn't connect to cdn-fastly.obsproject.com"); return false; } @@ -612,7 +614,7 @@ static inline bool has_str(const char *file, const char *str) #define UTF8ToWideBuf(wide, utf8) UTF8ToWide(wide, _countof(wide), utf8) #define WideToUTF8Buf(utf8, wide) WideToUTF8(utf8, _countof(utf8), wide) -#define UPDATE_URL L"https://obsproject.com/update_studio" +#define UPDATE_URL L"https://cdn-fastly.obsproject.com/update_studio" static bool AddPackageUpdateFiles(json_t *root, size_t idx, const wchar_t *tempPath) @@ -748,7 +750,7 @@ static void UpdateWithPatchIfAvailable(const char *name, const char *hash, wchar_t sourceURL[1024]; wchar_t patchHashStr[BLAKE2_HASH_STR_LENGTH]; - if (strncmp(source, "https://obsproject.com/", 23) != 0) + if (strncmp(source, "https://cdn-fastly.obsproject.com/", 34) != 0) return; string patchPackageName = name; @@ -957,10 +959,10 @@ static bool UpdateVS2017Redists(json_t *root) WinHttpSetOption(hSession, WINHTTP_OPTION_SECURE_PROTOCOLS, (LPVOID)&tlsProtocols, sizeof(tlsProtocols)); - HttpHandle hConnect = WinHttpConnect(hSession, L"obsproject.com", + HttpHandle hConnect = WinHttpConnect(hSession, L"cdn-fastly.obsproject.com", INTERNET_DEFAULT_HTTPS_PORT, 0); if (!hConnect) { - Status(L"Update failed: Couldn't connect to obsproject.com"); + Status(L"Update failed: Couldn't connect to cdn-fastly.obsproject.com"); return false; } @@ -981,7 +983,7 @@ static bool UpdateVS2017Redists(json_t *root) : L"vc2017redist_x64.exe"; wstring sourceURL; - sourceURL += L"https://obsproject.com/downloads/"; + sourceURL += L"https://cdn-fastly.obsproject.com/downloads/"; sourceURL += file; wstring destPath; diff --git a/UI/win-update/win-update.cpp b/UI/win-update/win-update.cpp index 510fddf..2bd77b8 100644 --- a/UI/win-update/win-update.cpp +++ b/UI/win-update/win-update.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -27,11 +28,15 @@ using namespace std; #define WIN_MANIFEST_URL "https://obsproject.com/update_studio/manifest.json" #endif +#ifndef WIN_WHATSNEW_URL +#define WIN_WHATSNEW_URL "https://obsproject.com/update_studio/whatsnew.json" +#endif + #ifndef WIN_UPDATER_URL #define WIN_UPDATER_URL "https://obsproject.com/update_studio/updater.exe" #endif -static HCRYPTPROV provider = 0; +static __declspec(thread) HCRYPTPROV provider = 0; #pragma pack(push, r1, 1) @@ -478,6 +483,32 @@ void GenerateGUID(string &guid) HashToString(junk, &guid[0]); } +string GetProgramGUID() +{ + static mutex m; + lock_guard lock(m); + + /* NOTE: this is an arbitrary random number that we use to count the + * number of unique OBS installations and is not associated with any + * kind of identifiable information */ + const char *pguid = config_get_string(GetGlobalConfig(), + "General", "InstallGUID"); + string guid; + if (pguid) + guid = pguid; + + if (guid.empty()) { + GenerateGUID(guid); + + if (!guid.empty()) + config_set_string(GetGlobalConfig(), + "General", "InstallGUID", + guid.c_str()); + } + + return guid; +} + void AutoUpdateThread::infoMsg(const QString &title, const QString &text) { OBSMessageBox::information(App()->GetMainWindow(), title, text); @@ -605,24 +636,7 @@ try { /* ----------------------------------- * * get current install GUID */ - /* NOTE: this is an arbitrary random number that we use to count the - * number of unique OBS installations and is not associated with any - * kind of identifiable information */ - const char *pguid = config_get_string(GetGlobalConfig(), - "General", "InstallGUID"); - string guid; - if (pguid) - guid = pguid; - - if (guid.empty()) { - GenerateGUID(guid); - - if (!guid.empty()) - config_set_string(GetGlobalConfig(), - "General", "InstallGUID", - guid.c_str()); - } - + string guid = GetProgramGUID(); if (!guid.empty()) { string header = "X-OBS2-GUID: "; header += guid; @@ -780,3 +794,101 @@ try { } catch (string text) { blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); } + +/* ------------------------------------------------------------------------ */ + +void WhatsNewInfoThread::run() +try { + long responseCode; + vector extraHeaders; + string text; + string error; + string signature; + CryptProvider localProvider; + BYTE whatsnewHash[BLAKE2_HASH_LENGTH]; + bool success; + + BPtr whatsnewPath = GetConfigPathPtr( + "obs-studio\\updates\\whatsnew.json"); + + /* ----------------------------------- * + * create signature provider */ + + if (!CryptAcquireContext(&localProvider, + nullptr, + MS_ENH_RSA_AES_PROV, + PROV_RSA_AES, + CRYPT_VERIFYCONTEXT)) + throw strprintf("CryptAcquireContext failed: %lu", + GetLastError()); + + provider = localProvider; + + /* ----------------------------------- * + * avoid downloading json again */ + + if (CalculateFileHash(whatsnewPath, whatsnewHash)) { + char hashString[BLAKE2_HASH_STR_LENGTH]; + HashToString(whatsnewHash, hashString); + + string header = "If-None-Match: "; + header += hashString; + extraHeaders.push_back(move(header)); + } + + /* ----------------------------------- * + * get current install GUID */ + + string guid = GetProgramGUID(); + + if (!guid.empty()) { + string header = "X-OBS2-GUID: "; + header += guid; + extraHeaders.push_back(move(header)); + } + + /* ----------------------------------- * + * get json from server */ + + success = GetRemoteFile(WIN_WHATSNEW_URL, text, error, &responseCode, + nullptr, nullptr, extraHeaders, &signature); + + if (!success || (responseCode != 200 && responseCode != 304)) { + if (responseCode == 404) + return; + + throw strprintf("Failed to fetch whatsnew file: %s", + error.c_str()); + } + + /* ----------------------------------- * + * verify file signature */ + + if (responseCode == 200) { + success = CheckDataSignature(text, "whatsnew", + signature.data(), signature.size()); + if (!success) + throw string("Invalid whatsnew signature"); + } + + /* ----------------------------------- * + * write or load json */ + + if (responseCode == 200) { + if (!QuickWriteFile(whatsnewPath, text.data(), text.size())) + throw strprintf("Could not write file '%s'", + whatsnewPath.Get()); + } else { + if (!QuickReadFile(whatsnewPath, text)) + throw strprintf("Could not read file '%s'", + whatsnewPath.Get()); + } + + /* ----------------------------------- * + * success */ + + emit Result(QString::fromUtf8(text.c_str())); + +} catch (string text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); +} diff --git a/UI/win-update/win-update.hpp b/UI/win-update/win-update.hpp index 47bdd03..c1bd8fb 100644 --- a/UI/win-update/win-update.hpp +++ b/UI/win-update/win-update.hpp @@ -21,3 +21,15 @@ private slots: public: AutoUpdateThread(bool manualUpdate_) : manualUpdate(manualUpdate_) {} }; + +class WhatsNewInfoThread : public QThread { + Q_OBJECT + + virtual void run() override; + +signals: + void Result(const QString &text); + +public: + inline WhatsNewInfoThread() {} +}; diff --git a/UI/window-basic-adv-audio.cpp b/UI/window-basic-adv-audio.cpp index 2894f6b..e6b47f7 100644 --- a/UI/window-basic-adv-audio.cpp +++ b/UI/window-basic-adv-audio.cpp @@ -6,6 +6,7 @@ #include #include "window-basic-adv-audio.hpp" #include "window-basic-main.hpp" +#include "item-widget-helpers.hpp" #include "adv-audio-control.hpp" #include "obs-app.hpp" #include "qt-wrappers.hpp" @@ -133,7 +134,12 @@ void OBSBasicAdvAudio::OBSSourceRemoved(void *param, calldata_t *calldata) inline void OBSBasicAdvAudio::AddAudioSource(obs_source_t *source) { OBSAdvAudioCtrl *control = new OBSAdvAudioCtrl(mainLayout, source); - controls.push_back(control); + + InsertQObjectByName(controls, control); + + for (auto control : controls) { + control->ShowAudioControl(mainLayout); + } } void OBSBasicAdvAudio::SourceAdded(OBSSource source) diff --git a/UI/window-basic-auto-config.cpp b/UI/window-basic-auto-config.cpp index 43e4c9a..7ddf2af 100644 --- a/UI/window-basic-auto-config.cpp +++ b/UI/window-basic-auto-config.cpp @@ -578,7 +578,7 @@ AutoConfig::AutoConfig(QWidget *parent) setPage(VideoPage, new AutoConfigVideoPage()); setPage(StreamPage, streamPage); setPage(TestPage, new AutoConfigTestPage()); - setWindowTitle(QTStr("Basic.AutoConfig.Beta")); + setWindowTitle(QTStr("Basic.AutoConfig")); obs_video_info ovi; obs_get_video_info(&ovi); diff --git a/UI/window-basic-filters.cpp b/UI/window-basic-filters.cpp index d792b1e..564deac 100644 --- a/UI/window-basic-filters.cpp +++ b/UI/window-basic-filters.cpp @@ -56,7 +56,8 @@ OBSBasicFilters::OBSBasicFilters(QWidget *parent, OBSSource source_) OBSBasicFilters::SourceRemoved, this), renameSourceSignal (obs_source_get_signal_handler(source), "rename", - OBSBasicFilters::SourceRenamed, this) + OBSBasicFilters::SourceRenamed, this), + noPreviewMargin (13) { main = reinterpret_cast(parent); @@ -97,10 +98,10 @@ OBSBasicFilters::OBSBasicFilters(QWidget *parent, OBSSource source_) connect(ui->buttonBox->button(QDialogButtonBox::Reset), SIGNAL(clicked()), this, SLOT(ResetFilters())); - uint32_t flags = obs_source_get_output_flags(source); - bool audio = (flags & OBS_SOURCE_AUDIO) != 0; - bool audioOnly = (flags & OBS_SOURCE_VIDEO) == 0; - bool async = (flags & OBS_SOURCE_ASYNC) != 0; + uint32_t caps = obs_source_get_output_flags(source); + bool audio = (caps & OBS_SOURCE_AUDIO) != 0; + bool audioOnly = (caps & OBS_SOURCE_VIDEO) == 0; + bool async = (caps & OBS_SOURCE_ASYNC) != 0; if (!async && !audio) { ui->asyncWidget->setVisible(false); @@ -121,13 +122,20 @@ OBSBasicFilters::OBSBasicFilters(QWidget *parent, OBSSource source_) }; enum obs_source_type type = obs_source_get_type(source); - uint32_t caps = obs_source_get_output_flags(source); bool drawable_type = type == OBS_SOURCE_TYPE_INPUT || type == OBS_SOURCE_TYPE_SCENE; - if (drawable_type && (caps & OBS_SOURCE_VIDEO) != 0) - connect(ui->preview, &OBSQTDisplay::DisplayCreated, - addDrawCallback); + if ((caps & OBS_SOURCE_VIDEO) != 0) { + ui->rightLayout->setContentsMargins(0, 0, 0, 0); + ui->preview->show(); + if (drawable_type) + connect(ui->preview, &OBSQTDisplay::DisplayCreated, + addDrawCallback); + } else { + ui->rightLayout->setContentsMargins(0, noPreviewMargin, 0, 0); + ui->rightContainerLayout->insertStretch(1); + ui->preview->hide(); + } } OBSBasicFilters::~OBSBasicFilters() @@ -569,7 +577,7 @@ static bool QueryRemove(QWidget *parent, obs_source_t *source) void OBSBasicFilters::on_addAsyncFilter_clicked() { - QPointer popup = CreateAddFilterPopupMenu(true); + QScopedPointer popup(CreateAddFilterPopupMenu(true)); if (popup) popup->exec(QCursor::pos()); } @@ -611,7 +619,7 @@ void OBSBasicFilters::on_asyncFilters_currentRowChanged(int row) void OBSBasicFilters::on_addEffectFilter_clicked() { - QPointer popup = CreateAddFilterPopupMenu(false); + QScopedPointer popup(CreateAddFilterPopupMenu(false)); if (popup) popup->exec(QCursor::pos()); } diff --git a/UI/window-basic-filters.hpp b/UI/window-basic-filters.hpp index cc655a4..9fdbce3 100644 --- a/UI/window-basic-filters.hpp +++ b/UI/window-basic-filters.hpp @@ -72,6 +72,8 @@ private: bool isAsync; + int noPreviewMargin; + private slots: void AddFilter(OBSSource filter); void RemoveFilter(OBSSource filter); diff --git a/UI/window-basic-main-dropfiles.cpp b/UI/window-basic-main-dropfiles.cpp index 2622ab1..cd5c481 100644 --- a/UI/window-basic-main-dropfiles.cpp +++ b/UI/window-basic-main-dropfiles.cpp @@ -107,8 +107,10 @@ void OBSBasic::AddDropSource(const char *data, DropType image) break; } - if (!obs_source_get_display_name(type)) + if (!obs_source_get_display_name(type)) { + obs_data_release(settings); return; + } if (name.isEmpty()) name = obs_source_get_display_name(type); diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp index 16039a3..2f9bfa8 100644 --- a/UI/window-basic-main-outputs.cpp +++ b/UI/window-basic-main-outputs.cpp @@ -895,6 +895,7 @@ bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) obs_data_set_string(settings, "directory", path); obs_data_set_string(settings, "format", f.c_str()); obs_data_set_string(settings, "extension", format); + obs_data_set_bool(settings, "allow_spaces", !noSpace); obs_data_set_int(settings, "max_time_sec", rbTime); obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); @@ -1032,19 +1033,32 @@ struct AdvancedOutput : BasicOutputHandler { static OBSData GetDataFromJsonFile(const char *jsonFile) { char fullPath[512]; + obs_data_t *data = nullptr; int ret = GetProfilePath(fullPath, sizeof(fullPath), jsonFile); if (ret > 0) { BPtr jsonData = os_quick_read_utf8_file(fullPath); if (!!jsonData) { - obs_data_t *data = obs_data_create_from_json(jsonData); - OBSData dataRet(data); - obs_data_release(data); - return dataRet; + data = obs_data_create_from_json(jsonData); } } - return nullptr; + if (!data) + data = obs_data_create(); + OBSData dataRet(data); + obs_data_release(data); + return dataRet; +} + +static void ApplyEncoderDefaults(OBSData &settings, + const obs_encoder_t *encoder) +{ + OBSData dataRet = obs_encoder_get_defaults(encoder); + obs_data_release(dataRet); + + if (!!settings) + obs_data_apply(dataRet, settings); + settings = std::move(dataRet); } AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) @@ -1157,6 +1171,7 @@ void AdvancedOutput::UpdateStreamSettings() "ApplyServiceSettings"); OBSData settings = GetDataFromJsonFile("streamEncoder.json"); + ApplyEncoderDefaults(settings, h264Streaming); if (applyServiceSettings) obs_service_apply_encoder_settings(main->GetService(), @@ -1717,6 +1732,7 @@ bool AdvancedOutput::StartReplayBuffer() obs_data_set_string(settings, "directory", path); obs_data_set_string(settings, "format", f.c_str()); obs_data_set_string(settings, "extension", recFormat); + obs_data_set_bool(settings, "allow_spaces", !noSpace); obs_data_set_int(settings, "max_time_sec", rbTime); obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); diff --git a/UI/window-basic-main-profiles.cpp b/UI/window-basic-main-profiles.cpp index 160e13c..f5db4cd 100644 --- a/UI/window-basic-main-profiles.cpp +++ b/UI/window-basic-main-profiles.cpp @@ -346,6 +346,19 @@ void OBSBasic::ResetProfileData() ResetOutputs(); ClearHotkeys(); CreateHotkeys(); + + /* load audio monitoring */ +#if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO + const char *device_name = config_get_string(basicConfig, "Audio", + "MonitoringDeviceName"); + const char *device_id = config_get_string(basicConfig, "Audio", + "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", + device_name, device_id); +#endif } void OBSBasic::on_actionNewProfile_triggered() diff --git a/UI/window-basic-main-scene-collections.cpp b/UI/window-basic-main-scene-collections.cpp index 113f848..9759a27 100644 --- a/UI/window-basic-main-scene-collections.cpp +++ b/UI/window-basic-main-scene-collections.cpp @@ -154,13 +154,19 @@ static bool GetSceneCollectionName(QWidget *parent, std::string &name, return true; } -void OBSBasic::AddSceneCollection(bool create_new) +bool OBSBasic::AddSceneCollection(bool create_new, const QString &qname) { std::string name; std::string file; - if (!GetSceneCollectionName(this, name, file)) - return; + if (qname.isEmpty()) { + if (!GetSceneCollectionName(this, name, file)) + return false; + } else { + name = QT_TO_UTF8(qname); + if (SceneCollectionExists(name.c_str())) + return false; + } SaveProjectNow(); @@ -185,6 +191,8 @@ void OBSBasic::AddSceneCollection(bool create_new) api->on_event(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); api->on_event(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); } + + return true; } void OBSBasic::RefreshSceneCollections() @@ -237,7 +245,6 @@ void OBSBasic::RefreshSceneCollections() OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - main->OpenSavedProjectors(); main->ui->actionPasteFilters->setEnabled(false); main->ui->actionPasteRef->setEnabled(false); main->ui->actionPasteDup->setEnabled(false); diff --git a/UI/window-basic-main-transitions.cpp b/UI/window-basic-main-transitions.cpp index 726eea1..952f5d4 100644 --- a/UI/window-basic-main-transitions.cpp +++ b/UI/window-basic-main-transitions.cpp @@ -26,6 +26,8 @@ #include "menu-button.hpp" #include "qt-wrappers.hpp" +#include "obs-hotkey.h" + using namespace std; Q_DECLARE_METATYPE(OBSScene); @@ -98,6 +100,18 @@ void OBSBasic::AddQuickTransitionHotkey(QuickTransition *qt) (void*)(uintptr_t)qt->id); } +void QuickTransition::SourceRenamed(void *param, calldata_t *data) +{ + QuickTransition *qt = reinterpret_cast(param); + + QString hotkeyName = QTStr("QuickTransitions.HotkeyName") + .arg(MakeQuickTransitionText(qt)); + + obs_hotkey_set_description(qt->hotkey, QT_TO_UTF8(hotkeyName)); + + UNUSED_PARAMETER(data); +} + void OBSBasic::TriggerQuickTransition(int id) { QuickTransition *qt = GetQuickTransition(id); @@ -545,6 +559,10 @@ void OBSBasic::RenameTransition() int idx = ui->transitions->findData(variant); if (idx != -1) { ui->transitions->setItemText(idx, QT_UTF8(name.c_str())); + + if (api) + api->on_event(OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED); + ClearQuickTransitionWidgets(); RefreshQuickTransitions(); } @@ -639,12 +657,11 @@ void OBSBasic::SetCurrentScene(OBSSource scene, bool force, bool direct) ui->scenes->blockSignals(true); ui->scenes->setCurrentItem(item); ui->scenes->blockSignals(false); + if (api) + api->on_event(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); break; } } - - if (api && IsPreviewProgramMode()) - api->on_event(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); } UpdateSceneSelection(scene); @@ -733,7 +750,7 @@ void OBSBasic::CreateProgramOptions() programOptions->setLayout(layout); auto onAdd = [this] () { - QPointer menu = CreateTransitionMenu(this, nullptr); + QScopedPointer menu(CreateTransitionMenu(this, nullptr)); menu->exec(QCursor::pos()); }; diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index e729b69..b2cebfe 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -27,6 +27,9 @@ #include #include #include +#include +#include +#include #include #include @@ -54,12 +57,14 @@ #include "display-helpers.hpp" #include "volume-control.hpp" #include "remote-text.hpp" +#include "source-tree.hpp" #ifdef _WIN32 #include "win-update/win-update.hpp" #endif #include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" #include #include @@ -67,8 +72,19 @@ #include #include +#ifdef _WIN32 +#include +#endif + +#include + +using namespace json11; using namespace std; +#ifdef _WIN32 +static CREATE_BROWSER_WIDGET_PROC create_browser_widget = nullptr; +#endif + namespace { template @@ -129,7 +145,27 @@ static void AddExtraModulePaths() #endif } -static QList DeleteKeys; +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +static int CountVideoSources() +{ + int count = 0; + + auto countSources = [] (void *param, obs_source_t *source) + { + if (!source) + return true; + + uint32_t flags = obs_source_get_output_flags(source); + if ((flags & OBS_SOURCE_VIDEO) != 0) + (*reinterpret_cast(param))++; + + return true; + }; + + obs_enum_sources(countSources, &count); + return count; +} OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow (parent), @@ -137,13 +173,10 @@ OBSBasic::OBSBasic(QWidget *parent) { setAttribute(Qt::WA_NativeWindow); - projectorArray.resize(10, ""); - previewProjectorArray.resize(10, 0); - multiviewProjectorArray.resize(10, 0); - studioProgramProjectorArray.resize(10, 0); - setAcceptDrops(true); + api = InitializeAPIInterface(this); + ui->setupUi(this); ui->previewDisabledLabel->setVisible(false); @@ -151,8 +184,6 @@ OBSBasic::OBSBasic(QWidget *parent) copyActionsDynamicProperties(); - ui->sources->setItemDelegate(new VisibilityItemDelegate(ui->sources)); - char styleSheetPath[512]; int ret = GetProfilePath(styleSheetPath, sizeof(styleSheetPath), "stylesheet.qss"); @@ -204,28 +235,15 @@ OBSBasic::OBSBasic(QWidget *parent) SLOT(SceneNameEdited(QWidget*, QAbstractItemDelegate::EndEditHint))); - connect(ui->sources->itemDelegate(), - SIGNAL(closeEditor(QWidget*, - QAbstractItemDelegate::EndEditHint)), - this, - SLOT(SceneItemNameEdited(QWidget*, - QAbstractItemDelegate::EndEditHint))); - cpuUsageInfo = os_cpu_usage_info_start(); cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer, SIGNAL(timeout()), + connect(cpuUsageTimer.data(), SIGNAL(timeout()), ui->statusbar, SLOT(UpdateCPUUsage())); cpuUsageTimer->start(3000); - DeleteKeys = #ifdef __APPLE__ - QList{{Qt::Key_Backspace}} << -#endif - QKeySequence::keyBindings(QKeySequence::Delete); - -#ifdef __APPLE__ - ui->actionRemoveSource->setShortcuts(DeleteKeys); - ui->actionRemoveScene->setShortcuts(DeleteKeys); + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); ui->action_Settings->setMenuRole(QAction::PreferencesRole); ui->actionE_xit->setMenuRole(QAction::QuitRole); @@ -245,7 +263,7 @@ OBSBasic::OBSBasic(QWidget *parent) addNudge(Qt::Key_Left, SLOT(NudgeLeft())); addNudge(Qt::Key_Right, SLOT(NudgeRight())); - auto assignDockToggle = [this](QDockWidget *dock, QAction *action) + auto assignDockToggle = [] (QDockWidget *dock, QAction *action) { auto handleWindowToggle = [action] (bool vis) { @@ -319,10 +337,7 @@ static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, OBSSource &curProgramScene, - obs_data_array_t *savedProjectorList, - obs_data_array_t *savedPreviewProjectorList, - obs_data_array_t *savedStudioProgramProjectorList, - obs_data_array_t *savedMultiviewProjectorList) + obs_data_array_t *savedProjectorList) { obs_data_t *saveData = obs_data_create(); @@ -335,8 +350,14 @@ static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + /* -------------------------------- */ + /* save non-group sources */ + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + return find(begin(audioSources), end(audioSources), source) == end(audioSources); }; @@ -348,6 +369,18 @@ static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, return (*static_cast(data))(source); }, static_cast(&FilterAudioSources)); + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void*, obs_source_t *source) + { + return obs_source_is_group(source); + }, nullptr); + + /* -------------------------------- */ + obs_source_t *transition = obs_get_output_source(0); obs_source_t *currentScene = obs_scene_get_source(scene); const char *sceneName = obs_source_get_name(currentScene); @@ -361,16 +394,12 @@ static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_set_array(saveData, "scene_order", sceneOrder); obs_data_set_string(saveData, "name", sceneCollection); obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); obs_data_set_array(saveData, "quick_transitions", quickTransitionData); obs_data_set_array(saveData, "transitions", transitions); obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_set_array(saveData, "saved_preview_projectors", - savedPreviewProjectorList); - obs_data_set_array(saveData, "saved_studio_preview_projectors", - savedStudioProgramProjectorList); - obs_data_set_array(saveData, "saved_multiview_projectors", - savedMultiviewProjectorList); obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); @@ -410,14 +439,33 @@ void OBSBasic::UpdateVolumeControlsDecayRate() } } -void OBSBasic::ClearVolumeControls() +void OBSBasic::UpdateVolumeControlsPeakMeterType() { - VolControl *control; + uint32_t peakMeterTypeIdx = config_get_uint(basicConfig, "Audio", + "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } for (size_t i = 0; i < volumes.size(); i++) { - control = volumes[i]; - delete control; + volumes[i]->setPeakMeterType(peakMeterType); } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; volumes.clear(); } @@ -439,62 +487,41 @@ obs_data_array_t *OBSBasic::SaveSceneListOrder() obs_data_array_t *OBSBasic::SaveProjectors() { - obs_data_array_t *saveProjector = obs_data_array_create(); + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; - for (size_t i = 0; i < projectorArray.size(); i++) { obs_data_t *data = obs_data_create(); - obs_data_set_string(data, "saved_projectors", - projectorArray.at(i).c_str()); - obs_data_array_push_back(saveProjector, data); + ProjectorType type = projector->GetProjectorType(); + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + obs_source_t *source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", + projector->saveGeometry().toBase64() + .constData()); + obs_data_array_push_back(savedProjectors, data); obs_data_release(data); - } + }; - return saveProjector; -} + for (QPointer &proj : projectors) + saveProjector(static_cast(proj.data())); -obs_data_array_t *OBSBasic::SavePreviewProjectors() -{ - obs_data_array_t *saveProjector = obs_data_array_create(); + for (QPointer &proj : windowProjectors) + saveProjector(static_cast(proj.data())); - for (size_t i = 0; i < previewProjectorArray.size(); i++) { - obs_data_t *data = obs_data_create(); - obs_data_set_int(data, "saved_preview_projectors", - previewProjectorArray.at(i)); - obs_data_array_push_back(saveProjector, data); - obs_data_release(data); - } - - return saveProjector; -} - -obs_data_array_t *OBSBasic::SaveStudioProgramProjectors() -{ - obs_data_array_t *saveProjector = obs_data_array_create(); - - for (size_t i = 0; i < studioProgramProjectorArray.size(); i++) { - obs_data_t *data = obs_data_create(); - obs_data_set_int(data, "saved_studio_preview_projectors", - studioProgramProjectorArray.at(i)); - obs_data_array_push_back(saveProjector, data); - obs_data_release(data); - } - - return saveProjector; -} - -obs_data_array_t *OBSBasic::SaveMultiviewProjectors() -{ - obs_data_array_t *saveProjector = obs_data_array_create(); - - for (size_t i = 0; i < multiviewProjectorArray.size(); i++) { - obs_data_t *data = obs_data_create(); - obs_data_set_int(data, "saved_multiview_projectors", - multiviewProjectorArray.at(i)); - obs_data_array_push_back(saveProjector, data); - obs_data_release(data); - } - - return saveProjector; + return savedProjectors; } void OBSBasic::Save(const char *file) @@ -508,17 +535,9 @@ void OBSBasic::Save(const char *file) obs_data_array_t *transitions = SaveTransitions(); obs_data_array_t *quickTrData = SaveQuickTransitions(); obs_data_array_t *savedProjectorList = SaveProjectors(); - obs_data_array_t *savedPreviewProjectorList = SavePreviewProjectors(); - obs_data_array_t *savedStudioProgramProjectorList = - SaveStudioProgramProjectors(); - obs_data_array_t *savedMultiviewProjectorList = - SaveMultiviewProjectors(); obs_data_t *saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), transitions, - scene, curProgramScene, savedProjectorList, - savedPreviewProjectorList, - savedStudioProgramProjectorList, - savedMultiviewProjectorList); + scene, curProgramScene, savedProjectorList); obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); obs_data_set_bool(saveData, "scaling_enabled", @@ -545,9 +564,19 @@ void OBSBasic::Save(const char *file) obs_data_array_release(quickTrData); obs_data_array_release(transitions); obs_data_array_release(savedProjectorList); - obs_data_array_release(savedPreviewProjectorList); - obs_data_array_release(savedStudioProgramProjectorList); - obs_data_array_release(savedMultiviewProjectorList); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } } static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) @@ -611,7 +640,6 @@ void OBSBasic::CreateDefaultScene(bool firstStart) if (firstStart) CreateFirstRunSources(); - AddScene(obs_scene_get_source(scene)); SetCurrentScene(scene, true); obs_scene_release(scene); @@ -653,47 +681,15 @@ void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) for (size_t i = 0; i < num; i++) { obs_data_t *data = obs_data_array_item(array, i); - projectorArray.at(i) = obs_data_get_string(data, - "saved_projectors"); - obs_data_release(data); - } -} - -void OBSBasic::LoadSavedPreviewProjectors(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - obs_data_t *data = obs_data_array_item(array, i); - previewProjectorArray.at(i) = obs_data_get_int(data, - "saved_preview_projectors"); - - obs_data_release(data); - } -} - -void OBSBasic::LoadSavedStudioProgramProjectors(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - obs_data_t *data = obs_data_array_item(array, i); - studioProgramProjectorArray.at(i) = obs_data_get_int(data, - "saved_studio_preview_projectors"); - - obs_data_release(data); - } -} - -void OBSBasic::LoadSavedMultiviewProjectors(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - obs_data_t *data = obs_data_array_item(array, i); - multiviewProjectorArray.at(i) = obs_data_get_int(data, - "saved_multiview_projectors"); + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, + "type")); + info->geometry = std::string( + obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + savedProjectorsArray.emplace_back(info); obs_data_release(data); } @@ -778,6 +774,7 @@ void OBSBasic::Load(const char *file) obs_data_array_t *sceneOrder = obs_data_get_array(data, "scene_order"); obs_data_array_t *sources = obs_data_get_array(data, "sources"); + obs_data_array_t *groups = obs_data_get_array(data, "groups"); obs_data_array_t *transitions= obs_data_get_array(data, "transitions"); const char *sceneName = obs_data_get_string(data, "current_scene"); @@ -818,7 +815,14 @@ void OBSBasic::Load(const char *file) LoadAudioDevice(AUX_AUDIO_2, 4, data); LoadAudioDevice(AUX_AUDIO_3, 5, data); - obs_load_sources(sources, OBSBasic::SourceLoaded, this); + if (!sources) { + sources = groups; + groups = nullptr; + } else { + obs_data_array_push_back_array(sources, groups); + } + + obs_load_sources(sources, nullptr, nullptr); if (transitions) LoadTransitions(transitions); @@ -834,47 +838,6 @@ void OBSBasic::Load(const char *file) ui->transitionDuration->setValue(newDuration); SetTransition(curTransition); - /* ------------------- */ - - obs_data_array_t *savedProjectors = obs_data_get_array(data, - "saved_projectors"); - - if (savedProjectors) - LoadSavedProjectors(savedProjectors); - - obs_data_array_release(savedProjectors); - - /* ------------------- */ - - obs_data_array_t *savedPreviewProjectors = obs_data_get_array(data, - "saved_preview_projectors"); - - if (savedPreviewProjectors) - LoadSavedPreviewProjectors(savedPreviewProjectors); - - obs_data_array_release(savedPreviewProjectors); - - /* ------------------- */ - - obs_data_array_t *savedStudioProgramProjectors = obs_data_get_array(data, - "saved_studio_preview_projectors"); - - if (savedStudioProgramProjectors) - LoadSavedStudioProgramProjectors(savedStudioProgramProjectors); - - obs_data_array_release(savedStudioProgramProjectors); - - /* ------------------- */ - - obs_data_array_t *savedMultiviewProjectors = obs_data_get_array(data, - "saved_multiview_projectors"); - - if (savedMultiviewProjectors) - LoadSavedMultiviewProjectors(savedMultiviewProjectors); - - obs_data_array_release(savedMultiviewProjectors); - - retryScene: curScene = obs_get_source_by_name(sceneName); curProgramScene = obs_get_source_by_name(programSceneName); @@ -903,8 +866,29 @@ retryScene: obs_source_release(curProgramScene); obs_data_array_release(sources); + obs_data_array_release(groups); obs_data_array_release(sceneOrder); + /* ------------------- */ + + bool projectorSave = config_get_bool(GetGlobalConfig(), "BasicWindow", + "SaveProjectors"); + + if (projectorSave) { + obs_data_array_t *savedProjectors = obs_data_get_array(data, + "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + + obs_data_array_release(savedProjectors); + } + + /* ------------------- */ + std::string file_base = strrchr(file, '/') + 1; file_base.erase(file_base.size() - 5, 5); @@ -972,8 +956,10 @@ retryScene: disableSaving--; - if (api) + if (api) { api->on_event(OBS_FRONTEND_EVENT_SCENE_CHANGED); + api->on_event(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + } } #define SERVICE_PATH "service.json" @@ -1247,6 +1233,7 @@ bool OBSBasic::InitBasicConfigDefaults() "Stereo"); config_set_default_double(basicConfig, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint (basicConfig, "Audio", "PeakMeterType", 0); return true; } @@ -1296,6 +1283,8 @@ void OBSBasic::InitOBSCallbacks() ProfileScope("OBSBasic::InitOBSCallbacks"); signalHandlers.reserve(signalHandlers.size() + 6); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", + OBSBasic::SourceCreated, this); signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", @@ -1377,6 +1366,7 @@ void OBSBasic::ResetOutputs() replayBufferButton = new QPushButton( QTStr("Basic.Main.StartReplayBuffer"), this); + replayBufferButton->setCheckable(true); connect(replayBufferButton.data(), &QPushButton::clicked, this, @@ -1402,8 +1392,6 @@ static void AddProjectorMenuMonitors(QMenu *parent, QObject *target, #define SHUTDOWN_SEPARATOR \ "==== Shutting down ==================================================" -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - #define UNSUPPORTED_ERROR \ "Failed to initialize video:\n\nRequired graphics API functionality " \ "not found. Your GPU may not be supported." @@ -1469,8 +1457,6 @@ void OBSBasic::OBSInit() InitOBSCallbacks(); InitHotkeys(); - api = InitializeAPIInterface(this); - AddExtraModulePaths(); blog(LOG_INFO, "---------------------------------"); obs_load_all_modules(); @@ -1479,6 +1465,10 @@ void OBSBasic::OBSInit() blog(LOG_INFO, "---------------------------------"); obs_post_load_modules(); +#ifdef _WIN32 + create_browser_widget = obs_browser_init_panel(); +#endif + CheckForSimpleModeX264Fallback(); blog(LOG_INFO, STARTUP_SEPARATOR); @@ -1520,12 +1510,14 @@ void OBSBasic::OBSInit() SET_VISIBILITY("ShowStatusBar", toggleStatusBar); #undef SET_VISIBILITY +#ifndef __APPLE__ { ProfileScope("OBSBasic::Load"); disableSaving--; Load(savePath); disableSaving++; } +#endif TimedCheckForUpdates(); loaded = true; @@ -1547,7 +1539,9 @@ void OBSBasic::OBSInit() } #endif +#ifndef __APPLE__ RefreshSceneCollections(); +#endif RefreshProfiles(); disableSaving--; @@ -1600,9 +1594,9 @@ void OBSBasic::OBSInit() ui->lockUI->setChecked(docksLocked); ui->lockUI->blockSignals(false); +#ifndef __APPLE__ SystemTray(true); - - OpenSavedProjectors(); +#endif if (windowState().testFlag(Qt::WindowFullScreen)) fullscreenInterface = true; @@ -1621,8 +1615,6 @@ void OBSBasic::OBSInit() if (!first_run && !has_last_version && !Active()) { QString msg; msg = QTStr("Basic.FirstStartup.RunWizard"); - msg += "\n\n"; - msg += QTStr("Basic.FirstStartup.RunWizard.BetaWarning"); QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("Basic.AutoConfig"), @@ -1637,6 +1629,9 @@ void OBSBasic::OBSInit() } } + ToggleMixerLayout(config_get_bool(App()->GlobalConfig(), "BasicWindow", + "VerticalVolControl")); + if (config_get_bool(basicConfig, "General", "OpenStatsOnStartup")) on_stats_triggered(); @@ -1664,6 +1659,174 @@ void OBSBasic::OBSInit() ui->menuCrashLogs = nullptr; ui->actionCheckForUpdates = nullptr; #endif + +#ifdef __APPLE__ + /* This is an incredibly unpleasant hack for macOS to isolate CEF + * initialization until after all tasks related to Qt startup and main + * window initialization have completed. There is a macOS-specific bug + * within either CEF and/or Qt that can cause a crash if both Qt and + * CEF are loading at the same time. + * + * CEF will typically load fine after about two iterations from this + * point, and all Qt tasks are typically fully completed after about + * four or five iterations, but to be "ultra" safe, an arbitrarily + * large number such as 10 is used. This hack is extremely unpleasant, + * but is worth doing instead of being forced to isolate the entire + * browser plugin in to a separate process as before. + * + * Again, this hack is specific to macOS only. Fortunately, on other + * operating systems, such issues do not occur. */ + QMetaObject::invokeMethod(this, "DeferredLoad", + Qt::QueuedConnection, + Q_ARG(QString, QT_UTF8(savePath)), + Q_ARG(int, 10)); +#else + OnFirstLoad(); +#endif +} + +void OBSBasic::OnFirstLoad() +{ + if (api) + api->on_event(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef _WIN32 + /* Attempt to load init screen if available */ + if (create_browser_widget) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + if (wnit) { + connect(wnit, &WhatsNewInfoThread::Result, + this, &OBSBasic::ReceivedIntroJson); + } + if (wnit) { + introCheckThread.reset(wnit); + introCheckThread->start(); + } + } +#endif +} + +void OBSBasic::DeferredLoad(const QString &file, int requeueCount) +{ + if (--requeueCount > 0) { + QMetaObject::invokeMethod(this, "DeferredLoad", + Qt::QueuedConnection, + Q_ARG(QString, file), + Q_ARG(int, requeueCount)); + return; + } + + Load(QT_TO_UTF8(file)); + RefreshSceneCollections(); + OnFirstLoad(); + + /* Minimizng to tray on initial startup does not work on mac + * unless it is done in the deferred load */ + SystemTray(true); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef _WIN32 + std::string err; + Json json = Json::parse(QT_TO_UTF8(text), err); + if (!err.empty()) + return; + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + const Json::array &items = json.array_items(); + for (const Json &item : items) { + const std::string &version = item["version"].string_value(); + const std::string &url = item["url"].string_value(); + int increment = item["increment"].int_value(); + int rc = item["RC"].int_value(); + + int major = 0; + int minor = 0; + + sscanf(version.c_str(), "%d.%d", &major, &minor); +#if OBS_RELEASE_CANDIDATE > 0 + if (major == OBS_RELEASE_CANDIDATE_MAJOR && + minor == OBS_RELEASE_CANDIDATE_MINOR && + rc == OBS_RELEASE_CANDIDATE) { +#else + if (major == LIBOBS_API_MAJOR_VER && + minor == LIBOBS_API_MINOR_VER && + rc == 0) { +#endif + info_url = url; + info_increment = increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + uint32_t lastVersion = config_get_int(App()->GlobalConfig(), "General", + "LastRCVersion"); +#else + uint32_t lastVersion = config_get_int(App()->GlobalConfig(), "General", + "LastVersion"); +#endif + + int current_version_increment = -1; + +#if OBS_RELEASE_CANDIDATE > 0 + if (lastVersion < OBS_RELEASE_CANDIDATE_VER) { +#else + if (lastVersion < LIBOBS_API_VER) { +#endif + config_set_int(App()->GlobalConfig(), "General", + "InfoIncrement", -1); + } else { + current_version_increment = config_get_int( + App()->GlobalConfig(), "General", + "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GlobalConfig(), "General", + "InfoIncrement", info_increment); + + QDialog dlg(this); + dlg.setWindowTitle("What's New"); + dlg.resize(700, 600); + + QCefWidget *cefWidget = create_browser_widget(nullptr, info_url); + if (!cefWidget) { + return; + } + + connect(cefWidget, SIGNAL(titleChanged(const QString &)), + &dlg, SLOT(setWindowTitle(const QString &))); + + QPushButton *close = new QPushButton(QTStr("Close")); + connect(close, &QAbstractButton::clicked, + &dlg, &QDialog::accept); + + QHBoxLayout *bottomLayout = new QHBoxLayout(); + bottomLayout->addStretch(); + bottomLayout->addWidget(close); + bottomLayout->addStretch(); + + QVBoxLayout *topLayout = new QVBoxLayout(&dlg); + topLayout->addWidget(cefWidget); + topLayout->addLayout(bottomLayout); + + dlg.exec(); +#else + UNUSED_PARAMETER(text); +#endif } void OBSBasic::UpdateMultiviewProjectorMenu() @@ -1786,7 +1949,7 @@ void OBSBasic::CreateHotkeys() [](void *data, obs_hotkey_pair_id, obs_hotkey_t*, bool pressed) \ { \ OBSBasic &basic = *static_cast(data); \ - if (pred && pressed) { \ + if ((pred) && pressed) { \ blog(LOG_INFO, log_action " due to hotkey"); \ method(); \ return true; \ @@ -1799,9 +1962,11 @@ void OBSBasic::CreateHotkeys() Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive(), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && + basic.ui->streamButton->isEnabled(), basic.StartStreaming, "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive(), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && + basic.ui->streamButton->isEnabled(), basic.StopStreaming, "Stopping stream"), this, this); LoadHotkeyPair(streamingHotkeys, @@ -1827,9 +1992,11 @@ void OBSBasic::CreateHotkeys() Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive(), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && + !basic.ui->recordButton->isChecked(), basic.StartRecording, "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive(), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && + basic.ui->recordButton->isChecked(), basic.StopRecording, "Stopping recording"), this, this); LoadHotkeyPair(recordingHotkeys, @@ -1894,6 +2061,8 @@ OBSBasic::~OBSBasic() if (updateCheckThread && updateCheckThread->isRunning()) updateCheckThread->wait(); + delete multiviewProjectorMenu; + delete trayMenu; delete programOptions; delete program; @@ -1950,6 +2119,10 @@ OBSBasic::~OBSBasic() config_set_int(App()->GlobalConfig(), "General", "LastVersion", LIBOBS_API_VER); +#if OBS_RELEASE_CANDIDATE > 0 + config_set_int(App()->GlobalConfig(), "General", "LastRCVersion", + OBS_RELEASE_CANDIDATE_VER); +#endif bool alwaysOnTop = IsAlwaysOnTop(this); @@ -2049,7 +2222,7 @@ OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) OBSSceneItem OBSBasic::GetCurrentSceneItem() { - return GetSceneItem(GetTopSelectedSourceItem()); + return ui->sources->Get(GetTopSelectedSourceItem()); } void OBSBasic::UpdatePreviewScalingMenu() @@ -2072,32 +2245,6 @@ void OBSBasic::UpdatePreviewScalingMenu() scalingAmount == float(ovi.output_width) / float(ovi.base_width)); } -void OBSBasic::UpdateSources(OBSScene scene) -{ - ClearListItems(ui->sources); - - obs_scene_enum_items(scene, - [] (obs_scene_t *scene, obs_sceneitem_t *item, void *p) - { - OBSBasic *window = static_cast(p); - window->InsertSceneItem(item); - - UNUSED_PARAMETER(scene); - return true; - }, this); -} - -void OBSBasic::InsertSceneItem(obs_sceneitem_t *item) -{ - QListWidgetItem *listItem = new QListWidgetItem(); - SetOBSRef(listItem, OBSSceneItem(item)); - - ui->sources->insertItem(0, listItem); - ui->sources->setCurrentRow(0, QItemSelectionModel::ClearAndSelect); - - SetupVisibilityItem(ui->sources, listItem, item); -} - void OBSBasic::CreateInteractionWindow(obs_source_t *source) { if (interaction) @@ -2161,8 +2308,6 @@ void OBSBasic::AddScene(OBSSource source) container.handlers.assign({ std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "item_remove", - OBSBasic::SceneItemRemoved, this), std::make_shared(handler, "item_select", OBSBasic::SceneItemSelected, this), std::make_shared(handler, "item_deselect", @@ -2224,7 +2369,7 @@ void OBSBasic::RemoveScene(OBSSource source) if (sel != nullptr) { if (sel == ui->scenes->currentItem()) - ClearListItems(ui->sources); + ui->sources->Clear(); delete sel; } @@ -2241,12 +2386,25 @@ void OBSBasic::RemoveScene(OBSSource source) api->on_event(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); } +static bool select_one(obs_scene_t *scene, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = + reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + UNUSED_PARAMETER(scene); + return true; +} + void OBSBasic::AddSceneItem(OBSSceneItem item) { obs_scene_t *scene = obs_sceneitem_get_scene(item); if (GetCurrentScene() == scene) - InsertSceneItem(item); + ui->sources->Add(item); SaveProject(); @@ -2257,30 +2415,8 @@ void OBSBasic::AddSceneItem(OBSSceneItem item) obs_source_get_name(itemSource), obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - } -} - -void OBSBasic::RemoveSceneItem(OBSSceneItem item) -{ - for (int i = 0; i < ui->sources->count(); i++) { - QListWidgetItem *listItem = ui->sources->item(i); - - if (GetOBSRef(listItem) == item) { - DeleteListItem(ui->sources, listItem); - break; - } - } - - SaveProject(); - - if (!disableSaving) { - obs_scene_t *scene = obs_sceneitem_get_scene(item); - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User Removed source '%s' (%s) from scene '%s'", - obs_source_get_name(itemSource), - obs_source_get_id(itemSource), - obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t*)item); } } @@ -2301,7 +2437,10 @@ void OBSBasic::UpdateSceneSelection(OBSSource source) ui->scenes->setCurrentItem(items.first()); sceneChanging = false; - UpdateSources(scene); + OBSScene curScene = + GetOBSRef(ui->scenes->currentItem()); + if (api && scene != curScene) + api->on_event(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); } } } @@ -2326,13 +2465,7 @@ void OBSBasic::RenameSources(OBSSource source, QString newName, volumes[i]->SetName(newName); } - std::string newText = newName.toUtf8().constData(); - std::string prevText = prevName.toUtf8().constData(); - - for (size_t j = 0; j < projectorArray.size(); j++) { - if (projectorArray.at(j) == prevText) - projectorArray.at(j) = newText; - } + OBSProjector::RenameProjector(prevName, newName); SaveProject(); @@ -2348,19 +2481,7 @@ void OBSBasic::SelectSceneItem(OBSScene scene, OBSSceneItem item, bool select) if (scene != GetCurrentScene() || ignoreSelectionUpdate) return; - for (int i = 0; i < ui->sources->count(); i++) { - QListWidgetItem *witem = ui->sources->item(i); - QVariant data = - witem->data(static_cast(QtDataRole::OBSRef)); - if (!data.canConvert()) - continue; - - if (item != data.value()) - continue; - - witem->setSelected(select); - break; - } + ui->sources->SelectItem(item, select); } static inline bool SourceMixerHidden(obs_source_t *source) @@ -2501,6 +2622,11 @@ void OBSBasic::VolControlContextMenu() QAction propertiesAction(QTStr("Properties"), this); QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked(config_get_bool(GetGlobalConfig(), + "BasicWindow", "VerticalVolControl")); + /* ------------------- */ connect(&hideAction, &QAction::triggered, @@ -2525,6 +2651,12 @@ void OBSBasic::VolControlContextMenu() /* ------------------- */ + connect(&toggleControlLayoutAction, &QAction::changed, this, + &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + hideAction.setProperty("volControl", QVariant::fromValue(vol)); mixerRenameAction.setProperty("volControl", @@ -2537,23 +2669,40 @@ void OBSBasic::VolControlContextMenu() /* ------------------- */ - QMenu popup(this); + QMenu popup; popup.addAction(&unhideAllAction); popup.addAction(&hideAction); popup.addAction(&mixerRenameAction); popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); popup.addAction(&filtersAction); popup.addAction(&propertiesAction); popup.addAction(&advPropAction); popup.exec(QCursor::pos()); } -void OBSBasic::on_mixerScrollArea_customContextMenuRequested() +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() { QAction unhideAllAction(QTStr("UnhideAll"), this); QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked(config_get_bool(GetGlobalConfig(), + "BasicWindow", "VerticalVolControl")); + /* ------------------- */ connect(&unhideAllAction, &QAction::triggered, @@ -2566,23 +2715,83 @@ void OBSBasic::on_mixerScrollArea_customContextMenuRequested() /* ------------------- */ - QMenu popup(this); + connect(&toggleControlLayoutAction, &QAction::changed, this, + &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; popup.addAction(&unhideAllAction); popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); popup.addAction(&advPropAction); popup.exec(QCursor::pos()); } +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(GetGlobalConfig(), "BasicWindow", + "VerticalVolControl"); + config_set_bool(GetGlobalConfig(), "BasicWindow", "VerticalVolControl", + vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + void OBSBasic::ActivateAudioSource(OBSSource source) { if (SourceMixerHidden(source)) return; - VolControl *vol = new VolControl(source, true); + bool vertical = config_get_bool(GetGlobalConfig(), "BasicWindow", + "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); double meterDecayRate = config_get_double(basicConfig, "Audio", "MeterDecayRate"); vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(basicConfig, "Audio", + "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + vol->setContextMenuPolicy(Qt::CustomContextMenu); connect(vol, &QWidget::customContextMenuRequested, @@ -2590,8 +2799,14 @@ void OBSBasic::ActivateAudioSource(OBSSource source) connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - volumes.push_back(vol); - ui->volumeWidgets->layout()->addWidget(vol); + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } } void OBSBasic::DeactivateAudioSource(OBSSource source) @@ -2607,7 +2822,8 @@ void OBSBasic::DeactivateAudioSource(OBSSource source) bool OBSBasic::QueryRemoveSource(obs_source_t *source) { - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE) { + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && + !obs_source_is_group(source)) { int count = ui->scenes->count(); if (count == 1) { @@ -2681,7 +2897,7 @@ void OBSBasic::CheckForUpdates(bool manualUpdate) if (updateCheckThread && updateCheckThread->isRunning()) return; - updateCheckThread = new AutoUpdateThread(manualUpdate); + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); updateCheckThread->start(); #endif @@ -2742,7 +2958,6 @@ void OBSBasic::DuplicateSelectedScene() obs_scene_t *scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); source = obs_scene_get_source(scene); - AddScene(source); SetCurrentScene(source, true); obs_scene_release(scene); @@ -2774,62 +2989,12 @@ void OBSBasic::RemoveSelectedSceneItem() } } -struct ReorderInfo { - int idx = 0; - OBSBasic *window; - - inline ReorderInfo(OBSBasic *window_) : window(window_) {} -}; - -void OBSBasic::ReorderSceneItem(obs_sceneitem_t *item, size_t idx) -{ - int count = ui->sources->count(); - int idx_inv = count - (int)idx - 1; - - for (int i = 0; i < count; i++) { - QListWidgetItem *listItem = ui->sources->item(i); - OBSSceneItem sceneItem = GetOBSRef(listItem); - - if (sceneItem == item) { - if ((int)idx_inv != i) { - bool sel = (ui->sources->currentRow() == i); - - listItem = TakeListItem(ui->sources, i); - if (listItem) { - ui->sources->insertItem(idx_inv, - listItem); - SetupVisibilityItem(ui->sources, - listItem, item); - - if (sel) - ui->sources->setCurrentRow( - idx_inv); - } - } - - break; - } - } -} - void OBSBasic::ReorderSources(OBSScene scene) { - ReorderInfo info(this); - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) return; - obs_scene_enum_items(scene, - [] (obs_scene_t*, obs_sceneitem_t *item, void *p) - { - ReorderInfo *info = - reinterpret_cast(p); - - info->window->ReorderSceneItem(item, - info->idx++); - return true; - }, &info); - + ui->sources->ReorderItems(); SaveProject(); } @@ -2855,16 +3020,6 @@ void OBSBasic::SceneItemAdded(void *data, calldata_t *params) Q_ARG(OBSSceneItem, OBSSceneItem(item))); } -void OBSBasic::SceneItemRemoved(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t*)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "RemoveSceneItem", - Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - void OBSBasic::SceneItemSelected(void *data, calldata_t *params) { OBSBasic *window = static_cast(data); @@ -2890,13 +3045,13 @@ void OBSBasic::SceneItemDeselected(void *data, calldata_t *params) } -void OBSBasic::SourceLoaded(void *data, obs_source_t *source) +void OBSBasic::SourceCreated(void *data, calldata_t *params) { - OBSBasic *window = static_cast(data); + obs_source_t *source = (obs_source_t*)calldata_ptr(params, "source"); if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(window, - "AddScene", + QMetaObject::invokeMethod(static_cast(data), + "AddScene", WaitConnection(), Q_ARG(OBSSource, OBSSource(source))); } @@ -3195,8 +3350,10 @@ int OBSBasic::ResetVideo() ResizeProgram(ovi.base_width, ovi.base_height); } - if (ret == OBS_VIDEO_SUCCESS) + if (ret == OBS_VIDEO_SUCCESS) { OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + } return ret; } @@ -3318,6 +3475,7 @@ void OBSBasic::CloseDialogs() } if (!stats.isNull()) stats->close(); //call close to save Stats geometry + if (!remux.isNull()) remux->close(); } void OBSBasic::EnumDialogs() @@ -3351,7 +3509,7 @@ void OBSBasic::ClearSceneData() ClearVolumeControls(); ClearListItems(ui->scenes); - ClearListItems(ui->sources); + ui->sources->Clear(); ClearQuickTransitions(); ui->transitions->clear(); @@ -3413,6 +3571,8 @@ void OBSBasic::closeEvent(QCloseEvent *event) blog(LOG_INFO, SHUTDOWN_SEPARATOR); + if (introCheckThread) + introCheckThread->wait(); if (updateCheckThread) updateCheckThread->wait(); if (logUploadThread) @@ -3449,20 +3609,33 @@ void OBSBasic::changeEvent(QEvent *event) void OBSBasic::on_actionShow_Recordings_triggered() { const char *mode = config_get_string(basicConfig, "Output", "Mode"); + const char *type = config_get_string(basicConfig, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") ? + config_get_string(basicConfig, "AdvOut", "FFFilePath") : + config_get_string(basicConfig, "AdvOut", "RecFilePath"); const char *path = strcmp(mode, "Advanced") ? - config_get_string(basicConfig, "SimpleOutput", "FilePath") : - config_get_string(basicConfig, "AdvOut", "RecFilePath"); + config_get_string(basicConfig, "SimpleOutput", "FilePath") : + adv_path; QDesktopServices::openUrl(QUrl::fromLocalFile(path)); } void OBSBasic::on_actionRemux_triggered() { + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + const char *mode = config_get_string(basicConfig, "Output", "Mode"); const char *path = strcmp(mode, "Advanced") ? config_get_string(basicConfig, "SimpleOutput", "FilePath") : config_get_string(basicConfig, "AdvOut", "RecFilePath"); - OBSRemux remux(path, this); - remux.exec(); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; } void OBSBasic::on_action_Settings_triggered() @@ -3514,6 +3687,9 @@ void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, SetCurrentScene(source); + if (api) + api->on_event(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + UNUSED_PARAMETER(prev); } @@ -3564,8 +3740,7 @@ void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) popup.addAction(QTStr("Rename"), this, SLOT(EditSceneName())); popup.addAction(QTStr("Remove"), - this, SLOT(RemoveSelectedScene()), - DeleteKeys.front()); + this, SLOT(RemoveSelectedScene())); popup.addSeparator(); order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), @@ -3616,7 +3791,7 @@ void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) multiviewAction->setCheckable(true); multiviewAction->setChecked(show); - auto showInMultiview = [this] (OBSData data) + auto showInMultiview = [] (OBSData data) { bool show = obs_data_get_bool(data, "show_in_multiview"); @@ -3673,7 +3848,6 @@ void OBSBasic::on_actionAddScene_triggered() obs_scene_t *scene = obs_scene_create(name.c_str()); source = obs_scene_get_source(scene); - AddScene(source); SetCurrentScene(source); obs_scene_release(scene); } @@ -3706,6 +3880,8 @@ void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) item->setSelected(true); sceneChanging = false; + + OBSProjector::UpdateMultiviewProjectors(); } void OBSBasic::on_actionSceneUp_triggered() @@ -3729,44 +3905,10 @@ void OBSBasic::MoveSceneToBottom() ui->scenes->count() - 1); } -void OBSBasic::on_sources_itemSelectionChanged() -{ - SignalBlocker sourcesSignalBlocker(ui->sources); - - auto updateItemSelection = [&]() - { - ignoreSelectionUpdate = true; - for (int i = 0; i < ui->sources->count(); i++) - { - QListWidgetItem *wItem = ui->sources->item(i); - OBSSceneItem item = GetOBSRef(wItem); - - obs_sceneitem_select(item, wItem->isSelected()); - } - ignoreSelectionUpdate = false; - }; - using updateItemSelection_t = decltype(updateItemSelection); - - obs_scene_atomic_update(GetCurrentScene(), - [](void *data, obs_scene_t *) - { - (*static_cast(data))(); - }, static_cast(&updateItemSelection)); -} - void OBSBasic::EditSceneItemName() { - QListWidgetItem *item = GetTopSelectedSourceItem(); - Qt::ItemFlags flags = item->flags(); - OBSSceneItem sceneItem= GetOBSRef(item); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - const char *name = obs_source_get_name(source); - - item->setText(QT_UTF8(name)); - item->setFlags(flags | Qt::ItemIsEditable); - ui->sources->removeItemWidget(item); - ui->sources->editItem(item); - item->setFlags(flags); + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); } void OBSBasic::SetDeinterlacingMode() @@ -3866,7 +4008,64 @@ QMenu *OBSBasic::AddScaleFilteringMenu(obs_sceneitem_t *item) return menu; } -void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview) +QMenu *OBSBasic::AddBackgroundColorMenu(obs_sceneitem_t *item) +{ + QMenu *menu = new QMenu(QTStr("ChangeBG")); + QAction *action; + + menu->setStyleSheet(QString( + "*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" \ + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" \ + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" \ + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" \ + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" \ + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" \ + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" \ + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, + + SLOT(ColorChange())); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, + + SLOT(ColorChange())); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + QWidgetAction *widgetAction = new QWidgetAction(menu); + ColorSelect *select = new ColorSelect(menu); + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild( + button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, SIGNAL(released()), this, + SLOT(ColorChange())); + } + + menu->addAction(widgetAction); + + return menu; +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) { QMenu popup(this); QPointer previewProjector; @@ -3907,6 +4106,17 @@ void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview) ui->actionCopyFilters->setEnabled(false); ui->actionCopySource->setEnabled(false); + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), + ui->sources, SLOT(GroupSelectedItems())); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), + ui->sources, SLOT(UngroupSelectedGroups())); + } + popup.addSeparator(); popup.addAction(ui->actionCopySource); popup.addAction(ui->actionPasteRef); @@ -3918,11 +4128,11 @@ void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview) popup.addAction(ui->actionPasteFilters); popup.addSeparator(); - if (item) { + if (idx != -1) { if (addSourceMenu) popup.addSeparator(); - OBSSceneItem sceneItem = GetSceneItem(item); + OBSSceneItem sceneItem = ui->sources->Get(idx); obs_source_t *source = obs_sceneitem_get_source(sceneItem); uint32_t flags = obs_source_get_output_flags(source); bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == @@ -3931,11 +4141,11 @@ void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview) OBS_SOURCE_AUDIO; QAction *action; + popup.addMenu(AddBackgroundColorMenu(sceneItem)); popup.addAction(QTStr("Rename"), this, SLOT(EditSceneItemName())); popup.addAction(QTStr("Remove"), this, - SLOT(on_actionRemoveSource_triggered()), - DeleteKeys.front()); + SLOT(on_actionRemoveSource_triggered())); popup.addSeparator(); popup.addMenu(ui->orderMenu); popup.addMenu(ui->transformMenu); @@ -3985,6 +4195,8 @@ void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview) ui->actionCopyFilters->setEnabled(true); ui->actionCopySource->setEnabled(true); + } else { + ui->actionPasteFilters->setEnabled(false); } popup.exec(QCursor::pos()); @@ -3992,20 +4204,10 @@ void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview) void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) { - if (ui->scenes->count()) - CreateSourcePopupMenu(ui->sources->itemAt(pos), false); -} - -void OBSBasic::on_sources_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - OBSSceneItem item = GetSceneItem(witem); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } } void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) @@ -4031,7 +4233,7 @@ void OBSBasic::AddSource(const char *id) if (id && *id) { OBSBasicSourceSelect sourceSelect(this, id); sourceSelect.exec(); - if (sourceSelect.newSource) + if (sourceSelect.newSource && strcmp(id, "group") != 0) CreatePropertiesWindow(sourceSelect.newSource); } } @@ -4089,6 +4291,13 @@ QMenu *OBSBasic::CreateAddSourcePopupMenu() addSource(popup, "scene", Str("Basic.Scene")); + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setData(QT_UTF8("group")); + connect(addGroup, SIGNAL(triggered(bool)), + this, SLOT(AddSourceFromAction())); + popup->addAction(addGroup); + if (!foundDeprecated) { delete deprecated; deprecated = nullptr; @@ -4099,6 +4308,7 @@ QMenu *OBSBasic::CreateAddSourcePopupMenu() popup = nullptr; } else if (foundDeprecated) { + popup->addSeparator(); popup->addMenu(deprecated); } @@ -4124,7 +4334,7 @@ void OBSBasic::AddSourcePopupMenu(const QPoint &pos) return; } - QPointer popup = CreateAddSourcePopupMenu(); + QScopedPointer popup(CreateAddSourcePopupMenu()); if (popup) popup->exec(pos); } @@ -4134,20 +4344,24 @@ void OBSBasic::on_actionAddSource_triggered() AddSourcePopupMenu(QCursor::pos()); } +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = + *reinterpret_cast*>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + void OBSBasic::on_actionRemoveSource_triggered() { vector items; - auto func = [] (obs_scene_t *, obs_sceneitem_t *item, void *param) - { - vector &items = - *reinterpret_cast*>(param); - if (obs_sceneitem_selected(item)) - items.emplace_back(item); - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, &items); + obs_scene_enum_items(GetCurrentScene(), remove_items, &items); if (!items.size()) return; @@ -4274,14 +4488,13 @@ void OBSBasic::UploadLog(const char *subdir, const char *file) if (logUploadThread) { logUploadThread->wait(); - delete logUploadThread; } RemoteTextThread *thread = new RemoteTextThread( - "https://hastebin.com/documents", + "https://obsproject.com/logs/upload", "text/plain", ss.str().c_str()); - logUploadThread = thread; + logUploadThread.reset(thread); connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); logUploadThread->start(); @@ -4355,8 +4568,7 @@ void OBSBasic::logUploadFinished(const QString &text, const QString &error) } obs_data_t *returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = "https://hastebin.com/"; - resURL += obs_data_get_string(returnData, "key"); + string resURL = obs_data_get_string(returnData, "url"); QString logURL = resURL.c_str(); obs_data_release(returnData); @@ -4413,26 +4625,6 @@ void OBSBasic::SceneNameEdited(QWidget *editor, UNUSED_PARAMETER(endHint); } -void OBSBasic::SceneItemNameEdited(QWidget *editor, - QAbstractItemDelegate::EndEditHint endHint) -{ - OBSSceneItem item = GetCurrentSceneItem(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!item) - return; - - obs_source_t *source = obs_sceneitem_get_source(item); - RenameListItem(this, ui->sources, source, text); - - QListWidgetItem *listItem = ui->sources->currentItem(); - listItem->setText(QString()); - SetupVisibilityItem(ui->sources, listItem, item); - - UNUSED_PARAMETER(endHint); -} - void OBSBasic::OpenFilters() { OBSSceneItem item = GetCurrentSceneItem(); @@ -4770,7 +4962,9 @@ void OBSBasic::StartRecording() api->on_event(OBS_FRONTEND_EVENT_RECORDING_STARTING); SaveProject(); - outputHandler->StartRecording(); + + if (!outputHandler->StartRecording()) + ui->recordButton->setChecked(false); } void OBSBasic::RecordStopping() @@ -4870,6 +5064,11 @@ void OBSBasic::StartReplayBuffer() if (disableOutputsRef) return; + if (!NoSourcesConfirmation()) { + replayBufferButton->setChecked(false); + return; + } + obs_output_t *output = outputHandler->replayBuffer; obs_data_t *hotkeys = obs_hotkeys_save_output(output); obs_data_array_t *bindings = obs_data_get_array(hotkeys, @@ -4889,7 +5088,8 @@ void OBSBasic::StartReplayBuffer() api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); SaveProject(); - outputHandler->StartReplayBuffer(); + if (!outputHandler->StartReplayBuffer()) + replayBufferButton->setChecked(false); } void OBSBasic::ReplayBufferStopping() @@ -4999,6 +5199,28 @@ void OBSBasic::ReplayBufferStop(int code) OnDeactivate(); } +bool OBSBasic::NoSourcesConfirmation() +{ + if (CountVideoSources() == 0 && isVisible()) { + QString msg; + msg = QTStr("NoSources.Text"); + msg += "\n\n"; + msg += QTStr("NoSources.Text.AddSource"); + + QMessageBox messageBox(QMessageBox::Question, + QTStr("NoSources.title"), + msg, + QMessageBox::Yes | QMessageBox::No, + this); + messageBox.setDefaultButton(QMessageBox::No); + + if (QMessageBox::No == messageBox.exec()) + return false; + } + + return true; +} + void OBSBasic::on_streamButton_clicked() { if (outputHandler->StreamingActive()) { @@ -5011,12 +5233,19 @@ void OBSBasic::on_streamButton_clicked() QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text")); - if (button == QMessageBox::No) + if (button == QMessageBox::No) { + ui->streamButton->setChecked(true); return; + } } StopStreaming(); } else { + if (!NoSourcesConfirmation()) { + ui->streamButton->setChecked(false); + return; + } + bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow", "WarnBeforeStartingStream"); @@ -5026,8 +5255,10 @@ void OBSBasic::on_streamButton_clicked() QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text")); - if (button == QMessageBox::No) + if (button == QMessageBox::No) { + ui->streamButton->setChecked(false); return; + } } StartStreaming(); @@ -5036,10 +5267,16 @@ void OBSBasic::on_streamButton_clicked() void OBSBasic::on_recordButton_clicked() { - if (outputHandler->RecordingActive()) + if (outputHandler->RecordingActive()) { StopRecording(); - else + } else { + if (!NoSourcesConfirmation()) { + ui->recordButton->setChecked(false); + return; + } + StartRecording(); + } } void OBSBasic::on_settingsButton_clicked() @@ -5059,6 +5296,12 @@ void OBSBasic::on_actionWebsite_triggered() QDesktopServices::openUrl(url); } +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + void OBSBasic::on_actionShowSettingsFolder_triggered() { char path[512]; @@ -5079,13 +5322,11 @@ void OBSBasic::on_actionShowProfileFolder_triggered() QDesktopServices::openUrl(QUrl::fromLocalFile(path)); } -QListWidgetItem *OBSBasic::GetTopSelectedSourceItem() +int OBSBasic::GetTopSelectedSourceItem() { - QList selectedItems = ui->sources->selectedItems(); - QListWidgetItem *topItem = nullptr; - if (selectedItems.size() != 0) - topItem = selectedItems[0]; - return topItem; + QModelIndexList selectedItems = + ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; } void OBSBasic::on_preview_customContextMenuRequested(const QPoint &pos) @@ -5145,12 +5386,14 @@ void OBSBasic::on_previewDisabledLabel_customContextMenuRequested( void OBSBasic::on_actionAlwaysOnTop_triggered() { - CloseDialogs(); - +#ifndef _WIN32 /* Make sure all dialogs are safely and successfully closed before * switching the always on top mode due to the fact that windows all * have to be recreated, so queue the actual toggle to happen after * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); } @@ -5290,36 +5533,38 @@ void OBSBasic::on_actionPasteTransform_triggered() obs_scene_enum_items(GetCurrentScene(), func, nullptr); } +static bool reset_tr(obs_scene_t *scene, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + UNUSED_PARAMETER(scene); + UNUSED_PARAMETER(param); + return true; +} + void OBSBasic::on_actionResetTransform_triggered() { - auto func = [] (obs_scene_t *scene, obs_sceneitem_t *item, void *param) - { - if (!obs_sceneitem_selected(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - UNUSED_PARAMETER(scene); - UNUSED_PARAMETER(param); - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, nullptr); + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); } static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) @@ -5367,6 +5612,9 @@ static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) static bool RotateSelectedSources(obs_scene_t *scene, obs_sceneitem_t *item, void *param) { + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, + param); if (!obs_sceneitem_selected(item)) return true; @@ -5379,10 +5627,11 @@ static bool RotateSelectedSources(obs_scene_t *scene, obs_sceneitem_t *item, else if (rot <= -360.0f) rot += 360.0f; obs_sceneitem_set_rot(item, rot); + obs_sceneitem_force_update_transform(item); + SetItemTL(item, tl); UNUSED_PARAMETER(scene); - UNUSED_PARAMETER(param); return true; }; @@ -5409,6 +5658,9 @@ static bool MultiplySelectedItemScale(obs_scene_t *scene, obs_sceneitem_t *item, { vec2 &mul = *reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, + param); if (!obs_sceneitem_selected(item)) return true; @@ -5419,6 +5671,8 @@ static bool MultiplySelectedItemScale(obs_scene_t *scene, obs_sceneitem_t *item, vec2_mul(&scale, &scale, &mul); obs_sceneitem_set_scale(item, &scale); + obs_sceneitem_force_update_transform(item); + SetItemTL(item, tl); UNUSED_PARAMETER(scene); @@ -5446,6 +5700,9 @@ static bool CenterAlignSelectedItems(obs_scene_t *scene, obs_sceneitem_t *item, { obs_bounds_type boundsType = *reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, + param); if (!obs_sceneitem_selected(item)) return true; @@ -5483,39 +5740,38 @@ void OBSBasic::on_actionStretchToScreen_triggered() &boundsType); } +static bool center_to_scene(obs_scene_t *, obs_sceneitem_t *item, void *) +{ + vec3 tl, br, itemCenter, screenCenter, offset; + obs_video_info ovi; + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, center_to_scene, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + + obs_get_video_info(&ovi); + + vec3_set(&screenCenter, float(ovi.base_width), + float(ovi.base_height), 0.0f); + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + GetItemBox(item, tl, br); + + vec3_sub(&itemCenter, &br, &tl); + vec3_mulf(&itemCenter, &itemCenter, 0.5f); + vec3_add(&itemCenter, &itemCenter, &tl); + + vec3_sub(&offset, &screenCenter, &itemCenter); + vec3_add(&tl, &tl, &offset); + + SetItemTL(item, tl); + return true; +}; + void OBSBasic::on_actionCenterToScreen_triggered() { - auto func = [] (obs_scene_t *scene, obs_sceneitem_t *item, void *param) - { - vec3 tl, br, itemCenter, screenCenter, offset; - obs_video_info ovi; - - if (!obs_sceneitem_selected(item)) - return true; - - obs_get_video_info(&ovi); - - vec3_set(&screenCenter, float(ovi.base_width), - float(ovi.base_height), 0.0f); - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - GetItemBox(item, tl, br); - - vec3_sub(&itemCenter, &br, &tl); - vec3_mulf(&itemCenter, &itemCenter, 0.5f); - vec3_add(&itemCenter, &itemCenter, &tl); - - vec3_sub(&offset, &screenCenter, &itemCenter); - vec3_add(&tl, &tl, &offset); - - SetItemTL(item, tl); - - UNUSED_PARAMETER(scene); - UNUSED_PARAMETER(param); - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, nullptr); + obs_scene_enum_items(GetCurrentScene(), center_to_scene, nullptr); } void OBSBasic::EnablePreviewDisplay(bool enable) @@ -5531,44 +5787,56 @@ void OBSBasic::TogglePreview() EnablePreviewDisplay(previewEnabled); } +static bool nudge_callback(obs_scene_t*, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, + &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + void OBSBasic::Nudge(int dist, MoveDir dir) { if (ui->preview->Locked()) return; - struct MoveInfo { - float dist; - MoveDir dir; - } info = {(float)dist, dir}; + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); - auto func = [] (obs_scene_t*, obs_sceneitem_t *item, void *param) - { - if (obs_sceneitem_locked(item)) - return true; + switch (dir) { + case MoveDir::Up: offset.y = (float)-dist; break; + case MoveDir::Down: offset.y = (float) dist; break; + case MoveDir::Left: offset.x = (float)-dist; break; + case MoveDir::Right: offset.x = (float) dist; break; + } - MoveInfo *info = reinterpret_cast(param); - struct vec2 dir; - struct vec2 pos; - - vec2_set(&dir, 0.0f, 0.0f); - - if (!obs_sceneitem_selected(item)) - return true; - - switch (info->dir) { - case MoveDir::Up: dir.y = -info->dist; break; - case MoveDir::Down: dir.y = info->dist; break; - case MoveDir::Left: dir.x = -info->dist; break; - case MoveDir::Right: dir.x = info->dist; break; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &dir); - obs_sceneitem_set_pos(item, &pos); - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, &info); + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); } void OBSBasic::NudgeUp() {Nudge(1, MoveDir::Up);} @@ -5576,40 +5844,17 @@ void OBSBasic::NudgeDown() {Nudge(1, MoveDir::Down);} void OBSBasic::NudgeLeft() {Nudge(1, MoveDir::Left);} void OBSBasic::NudgeRight() {Nudge(1, MoveDir::Right);} -void OBSBasic::OpenProjector(obs_source_t *source, int monitor, bool window, +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, QString title, ProjectorType type) { /* seriously? 10 monitors? */ if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return; + return nullptr; - if (!window) { - delete projectors[monitor]; - projectors[monitor].clear(); - RemoveSavedProjectors(monitor); - } - - OBSProjector *projector = new OBSProjector(nullptr, source, !!window); - const char *name = obs_source_get_name(source); - - if (!window) { - if (type == ProjectorType::StudioProgram) { - studioProgramProjectorArray.at((size_t)monitor) = 1; - } else if (type == ProjectorType::Preview) { - previewProjectorArray.at((size_t)monitor) = 1; - } else if (type == ProjectorType::Multiview) { - multiviewProjectorArray.at((size_t)monitor) = 1; - } else { - projectorArray.at((size_t)monitor) = name; - } - } - - if (!window) { - projector->Init(monitor, false, nullptr, type); - projectors[monitor] = projector; - } else { - projector->Init(monitor, true, title, type); + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, + title, type); + if (monitor < 0) { for (auto &projPtr : windowProjectors) { if (!projPtr) { projPtr = projector; @@ -5619,20 +5864,26 @@ void OBSBasic::OpenProjector(obs_source_t *source, int monitor, bool window, if (projector) windowProjectors.push_back(projector); + } else { + delete projectors[monitor]; + projectors[monitor].clear(); + + projectors[monitor] = projector; } + + return projector; } void OBSBasic::OpenStudioProgramProjector() { int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, false, nullptr, - ProjectorType::StudioProgram); + OpenProjector(nullptr, monitor, nullptr, ProjectorType::StudioProgram); } void OBSBasic::OpenPreviewProjector() { int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, false, nullptr, ProjectorType::Preview); + OpenProjector(nullptr, monitor, nullptr, ProjectorType::Preview); } void OBSBasic::OpenSourceProjector() @@ -5642,14 +5893,14 @@ void OBSBasic::OpenSourceProjector() if (!item) return; - OpenProjector(obs_sceneitem_get_source(item), monitor, false); + OpenProjector(obs_sceneitem_get_source(item), monitor, nullptr, + ProjectorType::Source); } void OBSBasic::OpenMultiviewProjector() { int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, false, nullptr, - ProjectorType::Multiview); + OpenProjector(nullptr, monitor, nullptr, ProjectorType::Multiview); } void OBSBasic::OpenSceneProjector() @@ -5659,114 +5910,112 @@ void OBSBasic::OpenSceneProjector() if (!scene) return; - OpenProjector(obs_scene_get_source(scene), monitor, false); + OpenProjector(obs_scene_get_source(scene), monitor, nullptr, + ProjectorType::Scene); } void OBSBasic::OpenStudioProgramWindow() { - int monitor = sender()->property("monitor").toInt(); - QString title = QTStr("StudioProgramWindow"); - OpenProjector(nullptr, monitor, true, title, + OpenProjector(nullptr, -1, QTStr("StudioProgramWindow"), ProjectorType::StudioProgram); } void OBSBasic::OpenPreviewWindow() { - int monitor = sender()->property("monitor").toInt(); - QString title = QTStr("PreviewWindow"); - OpenProjector(nullptr, monitor, true, nullptr, ProjectorType::Preview); + OpenProjector(nullptr, -1, QTStr("PreviewWindow"), + ProjectorType::Preview); } void OBSBasic::OpenSourceWindow() { - int monitor = sender()->property("monitor").toInt(); OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - QString text = QString::fromUtf8(obs_source_get_name(source)); - - QString title = QTStr("SourceWindow") + " - " + text; - if (!item) return; - OpenProjector(obs_sceneitem_get_source(item), monitor, true, title); + OBSSource source = obs_sceneitem_get_source(item); + QString title = QString::fromUtf8(obs_source_get_name(source)); + + OpenProjector(obs_sceneitem_get_source(item), -1, title, + ProjectorType::Source); } void OBSBasic::OpenMultiviewWindow() { - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, true, "Multiview", + OpenProjector(nullptr, -1, QTStr("MultiviewWindowed"), ProjectorType::Multiview); } void OBSBasic::OpenSceneWindow() { - int monitor = sender()->property("monitor").toInt(); OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - QString text = QString::fromUtf8(obs_source_get_name(source)); - - QString title = QTStr("SceneWindow") + " - " + text; - if (!scene) return; - OpenProjector(obs_scene_get_source(scene), monitor, true, title); + OBSSource source = obs_scene_get_source(scene); + QString title = QString::fromUtf8(obs_source_get_name(source)); + + OpenProjector(obs_scene_get_source(scene), -1, title, + ProjectorType::Scene); } void OBSBasic::OpenSavedProjectors() { - bool projectorSave = config_get_bool(GetGlobalConfig(), - "BasicWindow", "SaveProjectors"); + for (SavedProjectorInfo *info : savedProjectorsArray) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSource source = obs_get_source_by_name( + info->name.c_str()); + if (!source) + continue; - if (projectorSave) { - for (size_t i = 0; i < projectorArray.size(); i++) { - if (projectorArray.at(i).empty() == false) { - OBSSource source = obs_get_source_by_name( - projectorArray.at(i).c_str()); + QString title = nullptr; + if (info->monitor < 0) + title = QString::fromUtf8( + obs_source_get_name(source)); - if (!source) { - RemoveSavedProjectors((int)i); - obs_source_release(source); - continue; - } + projector = OpenProjector(source, info->monitor, title, + info->type); - OpenProjector(source, (int)i, false); - obs_source_release(source); - } + obs_source_release(source); + break; + } + case ProjectorType::Preview: { + projector = OpenProjector(nullptr, info->monitor, + QTStr("PreviewWindow"), + ProjectorType::Preview); + break; + } + case ProjectorType::StudioProgram: { + projector = OpenProjector(nullptr, info->monitor, + QTStr("StudioProgramWindow"), + ProjectorType::StudioProgram); + break; + } + case ProjectorType::Multiview: { + projector = OpenProjector(nullptr, info->monitor, + QTStr("MultiviewWindowed"), + ProjectorType::Multiview); + break; + } } - for (size_t i = 0; i < studioProgramProjectorArray.size(); i++) { - if (studioProgramProjectorArray.at(i) == 1) { - OpenProjector(nullptr, (int)i, false, nullptr, - ProjectorType::StudioProgram); - } - } + if (projector && !info->geometry.empty()) { + QByteArray byteArray = QByteArray::fromBase64( + QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); - for (size_t i = 0; i < previewProjectorArray.size(); i++) { - if (previewProjectorArray.at(i) == 1) { - OpenProjector(nullptr, (int)i, false, nullptr, - ProjectorType::Preview); - } - } - - for (size_t i = 0; i < multiviewProjectorArray.size(); i++) { - if (multiviewProjectorArray.at(i) == 1) { - OpenProjector(nullptr, (int)i, false, nullptr, - ProjectorType::Multiview); + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = App()->desktop()->geometry(); + projector->setGeometry(QStyle::alignedRect( + Qt::LeftToRight, + Qt::AlignCenter, size(), rect)); } } } } -void OBSBasic::RemoveSavedProjectors(int monitor) -{ - studioProgramProjectorArray.at((size_t)monitor) = 0; - multiviewProjectorArray.at((size_t)monitor) = 0; - previewProjectorArray.at((size_t)monitor) = 0; - projectorArray.at((size_t)monitor) = ""; -} - void OBSBasic::on_actionFullscreenInterface_triggered() { if (!fullscreenInterface) @@ -5978,6 +6227,10 @@ void OBSBasic::SetShowing(bool showing) setVisible(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + } else if (showing && !isVisible()) { if (showHide) showHide->setText(QTStr("Basic.SystemTray.Hide")); @@ -5988,6 +6241,14 @@ void OBSBasic::SetShowing(bool showing) setVisible(true); +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + /* show all child dialogs that was visible earlier */ if (!visDialogs.isEmpty()) { for (int i = 0; i < visDialogs.size(); ++i) { @@ -6022,25 +6283,26 @@ void OBSBasic::ToggleShowHide() void OBSBasic::SystemTrayInit() { - trayIcon = new QSystemTrayIcon(QIcon(":/res/images/obs.png"), - this); + trayIcon.reset(new QSystemTrayIcon(QIcon(":/res/images/obs.png"), + this)); trayIcon->setToolTip("OBS Studio"); showHide = new QAction(QTStr("Basic.SystemTray.Show"), - trayIcon); + trayIcon.data()); sysTrayStream = new QAction(QTStr("Basic.Main.StartStreaming"), - trayIcon); + trayIcon.data()); sysTrayRecord = new QAction(QTStr("Basic.Main.StartRecording"), - trayIcon); + trayIcon.data()); sysTrayReplayBuffer = new QAction(QTStr("Basic.Main.StartReplayBuffer"), - trayIcon); + trayIcon.data()); exit = new QAction(QTStr("Exit"), - trayIcon); + trayIcon.data()); if (outputHandler && !outputHandler->replayBuffer) sysTrayReplayBuffer->setEnabled(false); - connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), + connect(trayIcon.data(), + SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(IconActivated(QSystemTrayIcon::ActivationReason))); connect(showHide, SIGNAL(triggered()), @@ -6114,6 +6376,9 @@ void OBSBasic::SystemTray(bool firstStarted) QTimer::singleShot(50, this, SLOT(hide())); EnablePreviewDisplay(false); setVisible(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif opt_minimize_tray = false; } } else if (sysTrayEnabled) { @@ -6138,13 +6403,12 @@ bool OBSBasic::sysTrayMinimizeToTray() void OBSBasic::on_actionCopySource_triggered() { - on_actionCopyTransform_triggered(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) return; + on_actionCopyTransform_triggered(); + OBSSource source = obs_sceneitem_get_source(item); copyString = obs_source_get_name(source); @@ -6161,6 +6425,11 @@ void OBSBasic::on_actionCopySource_triggered() void OBSBasic::on_actionPasteRef_triggered() { + /* do not allow duplicate refs of the same group in the same scene */ + OBSScene scene = GetCurrentScene(); + if (!!obs_scene_get_group(scene, copyString)) + return; + OBSBasicSourceSelect::SourcePaste(copyString, copyVisible, false); on_actionPasteTransform_triggered(); } @@ -6199,6 +6468,180 @@ void OBSBasic::on_actionPasteFilters_triggered() obs_source_copy_filters(dstSource, source); } +static void ConfirmColor(SourceTree *sources, const QColor &color, + QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources + ->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get( + selectedItems[x].row()); + obs_data_t *privData = + obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", + QT_TO_UTF8(color.name( + QColor::HexArgb))); + obs_data_release(privData); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = + ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources + ->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get( + selectedItems[x].row()); + obs_data_t *privData = + obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + obs_data_release(privData); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = colorButton->parentWidget() + ->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = + GetItemWidgetFromSceneItem(curSceneItem); + obs_data_t *curPrivData = + obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int( + curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet( + "background: " + + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, + selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet( + "background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", + oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = + QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, + "color"); + const char *customColor = *oldColor != 0 ? oldColor + : "#55FF0000"; +#ifdef __APPLE__ + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor( + QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, + liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, + changedColor); + connect(colorDialog, &QColorDialog::rejected, + rejected); + colorDialog->open(); + + obs_data_release(curPrivData); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources + ->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get( + selectedItems[x].row()); + obs_data_t *privData = + obs_sceneitem_get_private_settings( + sceneItem); + obs_data_set_int(privData, "color-preset", + preset); + obs_data_set_string(privData, "color", ""); + obs_data_release(privData); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem( + obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if(treeItem) + return treeItem; + + return nullptr; +} + void OBSBasic::on_autoConfigure_triggered() { AutoConfig test(this); @@ -6220,3 +6663,10 @@ void OBSBasic::on_stats_triggered() statsDlg->show(); stats = statsDlg; } + +ColorSelect::ColorSelect(QWidget *parent) + : QWidget(parent), + ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 9b4302f..24e0036 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -29,6 +29,7 @@ #include "window-basic-transform.hpp" #include "window-basic-adv-audio.hpp" #include "window-basic-filters.hpp" +#include "window-projector.hpp" #include @@ -41,10 +42,10 @@ class QMessageBox; class QListWidgetItem; class VolControl; -class QNetworkReply; class OBSBasicStats; #include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" #define DESKTOP_AUDIO_1 Str("DesktopAudioDevice1") #define DESKTOP_AUDIO_2 Str("DesktopAudioDevice2") @@ -67,26 +68,33 @@ enum class QtDataRole { OBSSignals, }; -enum class ProjectorType { - Source, - Preview, - StudioProgram, - Multiview +struct SavedProjectorInfo { + ProjectorType type; + int monitor; + std::string geometry; + std::string name; }; struct QuickTransition { QPushButton *button = nullptr; OBSSource source; - obs_hotkey_id hotkey = 0; + obs_hotkey_id hotkey = OBS_INVALID_HOTKEY_ID; int duration = 0; int id = 0; inline QuickTransition() {} inline QuickTransition(OBSSource source_, int duration_, int id_) - : source (source_), - duration (duration_), - id (id_) + : source (source_), + duration (duration_), + id (id_), + renamedSignal (std::make_shared( + obs_source_get_signal_handler(source), + "rename", SourceRenamed, this)) {} + +private: + static void SourceRenamed(void *param, calldata_t *data); + std::shared_ptr renamedSignal; }; class OBSBasic : public OBSMainWindow { @@ -120,11 +128,6 @@ private: std::vector signalHandlers; - std::vector projectorArray; - std::vector studioProgramProjectorArray; - std::vector multiviewProjectorArray; - std::vector previewProjectorArray; - bool loaded = false; long disableSaving = 1; bool projectChanged = false; @@ -135,8 +138,9 @@ private: const char *copyFiltersString; bool copyVisible = true; - QPointer updateCheckThread; - QPointer logUploadThread; + QScopedPointer updateCheckThread; + QScopedPointer introCheckThread; + QScopedPointer logUploadThread; QPointer interaction; QPointer properties; @@ -169,16 +173,18 @@ private: ConfigFile basicConfig; + std::vector savedProjectorsArray; QPointer projectors[10]; QList> windowProjectors; QPointer stats; + QPointer remux; QPointer startStreamMenu; QPointer replayBufferButton; - QPointer trayIcon; + QScopedPointer trayIcon; QPointer sysTrayStream; QPointer sysTrayRecord; QPointer sysTrayReplayBuffer; @@ -197,6 +203,7 @@ private: void CreateDefaultScene(bool firstStart); void UpdateVolumeControlsDecayRate(); + void UpdateVolumeControlsPeakMeterType(); void ClearVolumeControls(); void UploadLog(const char *subdir, const char *file); @@ -217,6 +224,8 @@ private: void InitPrimitives(); + void OnFirstLoad(); + OBSSceneItem GetSceneItem(QListWidgetItem *item); OBSSceneItem GetCurrentSceneItem(); @@ -233,9 +242,6 @@ private: void UpdatePreviewScalingMenu(); - void UpdateSources(OBSScene scene); - void InsertSceneItem(obs_sceneitem_t *item); - void LoadSceneListOrder(obs_data_array_t *array); obs_data_array_t *SaveSceneListOrder(); void ChangeSceneIndex(bool relative, int idx, int invalidIdx); @@ -244,23 +250,20 @@ private: void TempStreamOutput(const char *url, const char *key, int vBitrate, int aBitrate); - void CreateInteractionWindow(obs_source_t *source); - void CreatePropertiesWindow(obs_source_t *source); - void CreateFiltersWindow(obs_source_t *source); - void CloseDialogs(); void ClearSceneData(); void Nudge(int dist, MoveDir dir); - void OpenProjector(obs_source_t *source, int monitor, bool window, - QString title = nullptr, - ProjectorType type = ProjectorType::Source); + + OBSProjector *OpenProjector(obs_source_t *source, int monitor, + QString title, ProjectorType type); void GetAudioSourceFilters(); void GetAudioSourceProperties(); void VolControlContextMenu(); + void ToggleVolControlLayout(); + void ToggleMixerLayout(bool vertical); - void AddSceneCollection(bool create_new); void RefreshSceneCollections(); void ChangeSceneCollection(); void LogScenes(); @@ -276,7 +279,7 @@ private: void SaveProjectNow(); - QListWidgetItem *GetTopSelectedSourceItem(); + int GetTopSelectedSourceItem(); obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, replayBufHotkeys; @@ -367,19 +370,14 @@ private: obs_data_array_t *SaveProjectors(); void LoadSavedProjectors(obs_data_array_t *savedProjectors); - obs_data_array_t *SavePreviewProjectors(); - void LoadSavedPreviewProjectors( - obs_data_array_t *savedPreviewProjectors); + void ReceivedIntroJson(const QString &text); - obs_data_array_t *SaveStudioProgramProjectors(); - void LoadSavedStudioProgramProjectors( - obs_data_array_t *savedStudioProgramProjectors); - - obs_data_array_t *SaveMultiviewProjectors(); - void LoadSavedMultiviewProjectors( - obs_data_array_t *savedMultiviewProjectors); + bool NoSourcesConfirmation(); public slots: + void DeferSaveBegin(); + void DeferSaveEnd(); + void StartStreaming(); void StopStreaming(); void ForceStopStreaming(); @@ -417,9 +415,12 @@ public slots: void SetCurrentScene(OBSSource scene, bool force = false, bool direct = false); + bool AddSceneCollection( + bool create_new, + const QString &name = QString()); + private slots: void AddSceneItem(OBSSceneItem item); - void RemoveSceneItem(OBSSceneItem item); void AddScene(OBSSource source); void RemoveScene(OBSSource source); void RenameSources(OBSSource source, QString newName, QString prevName); @@ -462,7 +463,8 @@ private slots: void MixerRenameSource(); - void on_mixerScrollArea_customContextMenuRequested(); + void on_vMixerScrollArea_customContextMenuRequested(); + void on_hMixerScrollArea_customContextMenuRequested(); void on_actionCopySource_triggered(); void on_actionPasteRef_triggered(); @@ -471,14 +473,17 @@ private slots: void on_actionCopyFilters_triggered(); void on_actionPasteFilters_triggered(); + void ColorChange(); + + SourceTreeItem *GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem); + private: /* OBS Callbacks */ static void SceneReordered(void *data, calldata_t *params); static void SceneItemAdded(void *data, calldata_t *params); - static void SceneItemRemoved(void *data, calldata_t *params); static void SceneItemSelected(void *data, calldata_t *params); static void SceneItemDeselected(void *data, calldata_t *params); - static void SourceLoaded(void *data, obs_source_t *source); + static void SourceCreated(void *data, calldata_t *params); static void SourceRemoved(void *data, calldata_t *params); static void SourceActivated(void *data, calldata_t *params); static void SourceDeactivated(void *data, calldata_t *params); @@ -537,6 +542,11 @@ public: cy = previewCY; } + inline bool SavingDisabled() const + { + return disableSaving; + } + inline double GetCPUUsage() const { return os_cpu_usage_info_query(cpuUsageInfo); @@ -555,11 +565,10 @@ public: } } - void ReorderSceneItem(obs_sceneitem_t *item, size_t idx); - QMenu *AddDeinterlacingMenu(obs_source_t *source); QMenu *AddScaleFilteringMenu(obs_sceneitem_t *item); - void CreateSourcePopupMenu(QListWidgetItem *item, bool preview); + QMenu *AddBackgroundColorMenu(obs_sceneitem_t *item); + void CreateSourcePopupMenu(int idx, bool preview); void UpdateTitleBar(); void UpdateSceneSelection(OBSSource source); @@ -568,7 +577,10 @@ public: void SystemTray(bool firstStarted); void OpenSavedProjectors(); - void RemoveSavedProjectors(int monitor); + + void CreateInteractionWindow(obs_source_t *source); + void CreatePropertiesWindow(obs_source_t *source); + void CreateFiltersWindow(obs_source_t *source); protected: virtual void closeEvent(QCloseEvent *event) override; @@ -611,9 +623,7 @@ private slots: void on_actionRemoveScene_triggered(); void on_actionSceneUp_triggered(); void on_actionSceneDown_triggered(); - void on_sources_itemSelectionChanged(); void on_sources_customContextMenuRequested(const QPoint &pos); - void on_sources_itemDoubleClicked(QListWidgetItem *item); void on_scenes_itemDoubleClicked(QListWidgetItem *item); void on_actionAddSource_triggered(); void on_actionRemoveSource_triggered(); @@ -640,6 +650,7 @@ private slots: void on_actionHelpPortal_triggered(); void on_actionWebsite_triggered(); + void on_actionDiscord_triggered(); void on_preview_customContextMenuRequested(const QPoint &pos); void on_program_customContextMenuRequested(const QPoint &pos); @@ -695,8 +706,6 @@ private slots: void SceneNameEdited(QWidget *editor, QAbstractItemDelegate::EndEditHint endHint); - void SceneItemNameEdited(QWidget *editor, - QAbstractItemDelegate::EndEditHint endHint); void OpenSceneFilters(); void OpenFilters(); @@ -721,6 +730,10 @@ private slots: void OpenMultiviewWindow(); void OpenSceneWindow(); + void DeferredLoad(const QString &file, int requeueCount); + + void StackedMixerAreaContextMenuRequested(); + public slots: void on_actionResetTransform_triggered(); @@ -738,3 +751,12 @@ public: private: std::unique_ptr ui; }; + +class ColorSelect : public QWidget { + +public: + explicit ColorSelect(QWidget *parent = 0); + +private: + std::unique_ptr ui; +}; diff --git a/UI/window-basic-preview.cpp b/UI/window-basic-preview.cpp index 8483060..bc4b2b9 100644 --- a/UI/window-basic-preview.cpp +++ b/UI/window-basic-preview.cpp @@ -39,6 +39,8 @@ struct SceneFindData { OBSSceneItem item; bool selectBelow; + obs_sceneitem_t *group = nullptr; + SceneFindData(const SceneFindData &) = delete; SceneFindData(SceneFindData &&) = delete; SceneFindData& operator=(const SceneFindData &) = delete; @@ -111,16 +113,6 @@ static vec3 GetTransformedPos(float x, float y, const matrix4 &mat) return result; } -static vec3 GetTransformedPosScaled(float x, float y, const matrix4 &mat, - float scale) -{ - vec3 result; - vec3_set(&result, x, y, 0.0f); - vec3_transform(&result, &result, &mat); - vec3_mulf(&result, &result, scale); - return result; -} - static inline vec2 GetOBSScreenSize() { obs_video_info ovi; @@ -214,11 +206,26 @@ static bool CheckItemSelected(obs_scene_t *scene, obs_sceneitem_t *item, if (!SceneItemHasVideo(item)) return true; + if (obs_sceneitem_is_group(item)) { + data->group = item; + obs_sceneitem_group_enum_items(item, CheckItemSelected, param); + data->group = nullptr; + + if (data->item) { + return false; + } + } vec3_set(&pos3, data->pos.x, data->pos.y, 0.0f); obs_sceneitem_get_box_transform(item, &transform); + if (data->group) { + matrix4 parent_transform; + obs_sceneitem_get_draw_transform(data->group, &parent_transform); + matrix4_mul(&transform, &transform, &parent_transform); + } + matrix4_inv(&transform, &transform); vec3_transform(&transformedPos, &pos3, &transform); @@ -248,8 +255,9 @@ bool OBSBasicPreview::SelectedAtPos(const vec2 &pos) } struct HandleFindData { - const vec2 &pos; - const float scale; + const vec2 &pos; + const float radius; + matrix4 parent_xform; OBSSceneItem item; ItemHandle handle = ItemHandle::None; @@ -259,38 +267,60 @@ struct HandleFindData { HandleFindData& operator=(const HandleFindData &) = delete; HandleFindData& operator=(HandleFindData &&) = delete; - inline HandleFindData(const vec2 &pos_, float scale_) - : pos (pos_), - scale (scale_) - {} + inline HandleFindData(const vec2 &pos_, float scale) + : pos (pos_), + radius (HANDLE_SEL_RADIUS / scale) + { + matrix4_identity(&parent_xform); + } + + inline HandleFindData(const HandleFindData &hfd, + obs_sceneitem_t *parent) + : pos (hfd.pos), + radius (hfd.radius), + item (hfd.item), + handle (hfd.handle) + { + obs_sceneitem_get_draw_transform(parent, &parent_xform); + } }; static bool FindHandleAtPos(obs_scene_t *scene, obs_sceneitem_t *item, void *param) { - if (!obs_sceneitem_selected(item)) - return true; + HandleFindData &data = *reinterpret_cast(param); + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + HandleFindData newData(data, item); + obs_sceneitem_group_enum_items(item, FindHandleAtPos, + &newData); + data.item = newData.item; + data.handle = newData.handle; + } + + return true; + } - HandleFindData *data = reinterpret_cast(param); matrix4 transform; vec3 pos3; - float closestHandle = HANDLE_SEL_RADIUS; + float closestHandle = data.radius; - vec3_set(&pos3, data->pos.x, data->pos.y, 0.0f); + vec3_set(&pos3, data.pos.x, data.pos.y, 0.0f); obs_sceneitem_get_box_transform(item, &transform); auto TestHandle = [&] (float x, float y, ItemHandle handle) { - vec3 handlePos = GetTransformedPosScaled(x, y, transform, - data->scale); + vec3 handlePos = GetTransformedPos(x, y, transform); + vec3_transform(&handlePos, &handlePos, &data.parent_xform); float dist = vec3_dist(&handlePos, &pos3); - if (dist < HANDLE_SEL_RADIUS) { + if (dist < data.radius) { if (dist < closestHandle) { closestHandle = dist; - data->handle = handle; - data->item = item; + data.handle = handle; + data.item = item; } } }; @@ -339,7 +369,10 @@ void OBSBasicPreview::GetStretchHandleData(const vec2 &pos) if (!scene) return; - HandleFindData data(pos, main->previewScale / main->devicePixelRatio()); + float scale = main->previewScale / main->devicePixelRatio(); + vec2 scaled_pos = pos; + vec2_divf(&scaled_pos, &scaled_pos, scale); + HandleFindData data(scaled_pos, scale); obs_scene_enum_items(scene, FindHandleAtPos, &data); stretchItem = std::move(data.item); @@ -377,6 +410,15 @@ void OBSBasicPreview::GetStretchHandleData(const vec2 &pos) startCrop.left - startCrop.right); cropSize.y = float(obs_source_get_height(source) - startCrop.top - startCrop.bottom); + + stretchGroup = obs_sceneitem_get_group(scene, stretchItem); + if (stretchGroup) { + obs_sceneitem_get_draw_transform(stretchGroup, + &invGroupTransform); + matrix4_inv(&invGroupTransform, + &invGroupTransform); + obs_sceneitem_defer_group_resize_begin(stretchGroup); + } } } @@ -482,6 +524,9 @@ static bool select_one(obs_scene_t *scene, obs_sceneitem_t *item, void *param) { obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + obs_sceneitem_select(item, (selectedItem == item)); UNUSED_PARAMETER(scene); @@ -534,10 +579,15 @@ void OBSBasicPreview::mouseReleaseEvent(QMouseEvent *event) if (!mouseMoved) ProcessClick(pos); - stretchItem = nullptr; - mouseDown = false; - mouseMoved = false; - cropping = false; + if (stretchGroup) { + obs_sceneitem_defer_group_resize_end(stretchGroup); + } + + stretchItem = nullptr; + stretchGroup = nullptr; + mouseDown = false; + mouseMoved = false; + cropping = false; } } @@ -550,30 +600,52 @@ static bool AddItemBounds(obs_scene_t *scene, obs_sceneitem_t *item, void *param) { SelectedItemBounds *data = reinterpret_cast(param); + vec3 t[4]; + auto add_bounds = [data, &t] () + { + for (const vec3 &v : t) { + if (data->first) { + vec3_copy(&data->tl, &v); + vec3_copy(&data->br, &v); + data->first = false; + } else { + vec3_min(&data->tl, &data->tl, &v); + vec3_max(&data->br, &data->br, &v); + } + } + }; + + if (obs_sceneitem_is_group(item)) { + SelectedItemBounds sib; + obs_sceneitem_group_enum_items(item, AddItemBounds, &sib); + + if (!sib.first) { + matrix4 xform; + obs_sceneitem_get_draw_transform(item, &xform); + + vec3_set(&t[0], sib.tl.x, sib.tl.y, 0.0f); + vec3_set(&t[1], sib.tl.x, sib.br.y, 0.0f); + vec3_set(&t[2], sib.br.x, sib.tl.y, 0.0f); + vec3_set(&t[3], sib.br.x, sib.br.y, 0.0f); + vec3_transform(&t[0], &t[0], &xform); + vec3_transform(&t[1], &t[1], &xform); + vec3_transform(&t[2], &t[2], &xform); + vec3_transform(&t[3], &t[3], &xform); + add_bounds(); + } + } if (!obs_sceneitem_selected(item)) return true; matrix4 boxTransform; obs_sceneitem_get_box_transform(item, &boxTransform); - vec3 t[4] = { - GetTransformedPos(0.0f, 0.0f, boxTransform), - GetTransformedPos(1.0f, 0.0f, boxTransform), - GetTransformedPos(0.0f, 1.0f, boxTransform), - GetTransformedPos(1.0f, 1.0f, boxTransform) - }; - - for (const vec3 &v : t) { - if (data->first) { - vec3_copy(&data->tl, &v); - vec3_copy(&data->br, &v); - data->first = false; - } else { - vec3_min(&data->tl, &data->tl, &v); - vec3_max(&data->br, &data->br, &v); - } - } + t[0] = GetTransformedPos(0.0f, 0.0f, boxTransform); + t[1] = GetTransformedPos(1.0f, 0.0f, boxTransform); + t[2] = GetTransformedPos(0.0f, 1.0f, boxTransform); + t[3] = GetTransformedPos(1.0f, 1.0f, boxTransform); + add_bounds(); UNUSED_PARAMETER(scene); return true; @@ -692,9 +764,22 @@ static bool move_items(obs_scene_t *scene, obs_sceneitem_t *item, void *param) if (obs_sceneitem_locked(item)) return true; + bool selected = obs_sceneitem_selected(item); vec2 *offset = reinterpret_cast(param); - if (obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item) && !selected) { + matrix4 transform; + vec3 new_offset; + vec3_set(&new_offset, offset->x, offset->y, 0.0f); + + obs_sceneitem_get_draw_transform(item, &transform); + vec4_set(&transform.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&transform, &transform); + vec3_transform(&new_offset, &new_offset, &transform); + obs_sceneitem_group_enum_items(item, move_items, &new_offset); + } + + if (selected) { vec2 pos; obs_sceneitem_get_pos(item, &pos); vec2_add(&pos, &pos, offset); @@ -856,9 +941,6 @@ void OBSBasicPreview::CropItem(const vec2 &pos) uint32_t align = obs_sceneitem_get_alignment(stretchItem); vec3 tl, br, pos3; - if (boundsType != OBS_BOUNDS_NONE) /* TODO */ - return; - vec3_zero(&tl); vec3_set(&br, stretchItemSize.x, stretchItemSize.y, 0.0f); @@ -963,7 +1045,8 @@ void OBSBasicPreview::CropItem(const vec2 &pos) obs_sceneitem_defer_update_begin(stretchItem); obs_sceneitem_set_crop(stretchItem, &crop); - obs_sceneitem_set_pos(stretchItem, (vec2*)&newPos); + if (boundsType == OBS_BOUNDS_NONE) + obs_sceneitem_set_pos(stretchItem, (vec2*)&newPos); obs_sceneitem_defer_update_end(stretchItem); } @@ -1063,6 +1146,20 @@ void OBSBasicPreview::mouseMoveEvent(QMouseEvent *event) pos.y = std::round(pos.y); if (stretchHandle != ItemHandle::None) { + OBSBasic *main = reinterpret_cast( + App()->GetMainWindow()); + OBSScene scene = main->GetCurrentScene(); + obs_sceneitem_t *group = obs_sceneitem_get_group( + scene, stretchItem); + if (group) { + vec3 group_pos; + vec3_set(&group_pos, pos.x, pos.y, 0.0f); + vec3_transform(&group_pos, &group_pos, + &invGroupTransform); + pos.x = group_pos.x; + pos.y = group_pos.y; + } + if (cropping) CropItem(pos); else @@ -1076,15 +1173,17 @@ void OBSBasicPreview::mouseMoveEvent(QMouseEvent *event) } } -static void DrawCircleAtPos(float x, float y, matrix4 &matrix, - float previewScale) +static void DrawCircleAtPos(float x, float y) { struct vec3 pos; vec3_set(&pos, x, y, 0.0f); + + struct matrix4 matrix; + gs_matrix_get(&matrix); vec3_transform(&pos, &pos, &matrix); - vec3_mulf(&pos, &pos, previewScale); gs_matrix_push(); + gs_matrix_identity(); gs_matrix_translate(&pos); gs_matrix_scale3f(HANDLE_RADIUS, HANDLE_RADIUS, 1.0f); gs_draw(GS_LINESTRIP, 0, 0); @@ -1108,6 +1207,16 @@ bool OBSBasicPreview::DrawSelectedItem(obs_scene_t *scene, if (!SceneItemHasVideo(item)) return true; + if (obs_sceneitem_is_group(item)) { + matrix4 mat; + obs_sceneitem_get_draw_transform(item, &mat); + + gs_matrix_push(); + gs_matrix_mul(&mat); + obs_sceneitem_group_enum_items(item, DrawSelectedItem, param); + gs_matrix_pop(); + } + if (!obs_sceneitem_selected(item)) return true; @@ -1142,19 +1251,18 @@ bool OBSBasicPreview::DrawSelectedItem(obs_scene_t *scene, gs_load_vertexbuffer(main->circle); - DrawCircleAtPos(0.0f, 0.0f, boxTransform, main->previewScale); - DrawCircleAtPos(0.0f, 1.0f, boxTransform, main->previewScale); - DrawCircleAtPos(1.0f, 0.0f, boxTransform, main->previewScale); - DrawCircleAtPos(1.0f, 1.0f, boxTransform, main->previewScale); - DrawCircleAtPos(0.5f, 0.0f, boxTransform, main->previewScale); - DrawCircleAtPos(0.0f, 0.5f, boxTransform, main->previewScale); - DrawCircleAtPos(0.5f, 1.0f, boxTransform, main->previewScale); - DrawCircleAtPos(1.0f, 0.5f, boxTransform, main->previewScale); - gs_matrix_push(); - gs_matrix_scale3f(main->previewScale, main->previewScale, 1.0f); gs_matrix_mul(&boxTransform); + DrawCircleAtPos(0.0f, 0.0f); + DrawCircleAtPos(0.0f, 1.0f); + DrawCircleAtPos(1.0f, 0.0f); + DrawCircleAtPos(1.0f, 1.0f); + DrawCircleAtPos(0.5f, 0.0f); + DrawCircleAtPos(0.0f, 0.5f); + DrawCircleAtPos(0.5f, 1.0f); + DrawCircleAtPos(1.0f, 0.5f); + obs_sceneitem_crop crop; obs_sceneitem_get_crop(item, &crop); @@ -1208,8 +1316,12 @@ void OBSBasicPreview::DrawSceneEditing() OBSScene scene = main->GetCurrentScene(); - if (scene) + if (scene) { + gs_matrix_push(); + gs_matrix_scale3f(main->previewScale, main->previewScale, 1.0f); obs_scene_enum_items(scene, DrawSelectedItem, this); + gs_matrix_pop(); + } gs_load_vertexbuffer(nullptr); @@ -1232,4 +1344,4 @@ void OBSBasicPreview::SetScalingAmount(float newScalingAmountVal) { scrollingOffset.x *= newScalingAmountVal / scalingAmount; scrollingOffset.y *= newScalingAmountVal / scalingAmount; scalingAmount = newScalingAmountVal; -} \ No newline at end of file +} diff --git a/UI/window-basic-preview.hpp b/UI/window-basic-preview.hpp index 4042281..c177dd9 100644 --- a/UI/window-basic-preview.hpp +++ b/UI/window-basic-preview.hpp @@ -35,11 +35,13 @@ private: obs_sceneitem_crop startCrop; vec2 startItemPos; vec2 cropSize; + OBSSceneItem stretchGroup; OBSSceneItem stretchItem; ItemHandle stretchHandle = ItemHandle::None; vec2 stretchItemSize; matrix4 screenToItem; matrix4 itemToScreen; + matrix4 invGroupTransform; vec2 startPos; vec2 lastMoveOffset; diff --git a/UI/window-basic-properties.cpp b/UI/window-basic-properties.cpp index d07336b..a03f187 100644 --- a/UI/window-basic-properties.cpp +++ b/UI/window-basic-properties.cpp @@ -114,15 +114,19 @@ OBSBasicProperties::OBSBasicProperties(QWidget *parent, OBSSource source_) obs_display_add_draw_callback(preview->GetDisplay(), OBSBasicProperties::DrawPreview, this); }; - enum obs_source_type type = obs_source_get_type(source); uint32_t caps = obs_source_get_output_flags(source); bool drawable_type = type == OBS_SOURCE_TYPE_INPUT || type == OBS_SOURCE_TYPE_SCENE; + bool drawable_preview = (caps & OBS_SOURCE_VIDEO) != 0; - if (drawable_type && (caps & OBS_SOURCE_VIDEO) != 0) + if (drawable_preview && drawable_type) { + preview->show(); connect(preview.data(), &OBSQTDisplay::DisplayCreated, addDrawCallback); + } else { + preview->hide(); + } } OBSBasicProperties::~OBSBasicProperties() diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index d1d8f48..4701b7b 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -318,6 +318,9 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->snapDistance, DSCROLL_CHANGED,GENERAL_CHANGED); HookWidget(ui->doubleClickSwitch, CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->studioPortraitLayout, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->multiviewMouseSwitch, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->multiviewDrawNames, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->multiviewDrawAreas, CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->multiviewLayout, COMBO_CHANGED, GENERAL_CHANGED); HookWidget(ui->outputMode, COMBO_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->streamType, COMBO_CHANGED, STREAM1_CHANGED); @@ -401,6 +404,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->channelSetup, COMBO_CHANGED, AUDIO_RESTART); HookWidget(ui->sampleRate, COMBO_CHANGED, AUDIO_RESTART); HookWidget(ui->meterDecayRate, COMBO_CHANGED, AUDIO_CHANGED); + HookWidget(ui->peakMeterType, COMBO_CHANGED, AUDIO_CHANGED); HookWidget(ui->desktopAudioDevice1, COMBO_CHANGED, AUDIO_CHANGED); HookWidget(ui->desktopAudioDevice2, COMBO_CHANGED, AUDIO_CHANGED); HookWidget(ui->auxAudioDevice1, COMBO_CHANGED, AUDIO_CHANGED); @@ -426,6 +430,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) #endif #ifdef _WIN32 HookWidget(ui->disableAudioDucking, CHECK_CHANGED, ADV_CHANGED); + HookWidget(ui->browserHWAccel, CHECK_CHANGED, ADV_RESTART); #endif HookWidget(ui->filenameFormatting, EDIT_CHANGED, ADV_CHANGED); HookWidget(ui->overwriteIfExists, CHECK_CHANGED, ADV_CHANGED); @@ -441,6 +446,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->bindToIP, COMBO_CHANGED, ADV_CHANGED); HookWidget(ui->enableNewSocketLoop, CHECK_CHANGED, ADV_CHANGED); HookWidget(ui->enableLowLatencyMode, CHECK_CHANGED, ADV_CHANGED); + HookWidget(ui->disableFocusHotkeys, CHECK_CHANGED, ADV_CHANGED); #if !defined(_WIN32) && !defined(__APPLE__) delete ui->enableAutoUpdates; @@ -495,6 +501,8 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) delete ui->advancedGeneralGroupBox; delete ui->enableNewSocketLoop; delete ui->enableLowLatencyMode; + delete ui->browserHWAccel; + delete ui->sourcesGroup; #if defined(__APPLE__) || HAVE_PULSEAUDIO delete ui->disableAudioDucking; #endif @@ -507,6 +515,8 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) ui->advancedGeneralGroupBox = nullptr; ui->enableNewSocketLoop = nullptr; ui->enableLowLatencyMode = nullptr; + ui->browserHWAccel = nullptr; + ui->sourcesGroup = nullptr; #if defined(__APPLE__) || HAVE_PULSEAUDIO ui->disableAudioDucking = nullptr; #endif @@ -701,11 +711,17 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) SimpleRecordingQualityChanged(); UpdateAutomaticReplayBufferCheckboxes(); + + App()->EnableInFocusHotkeys(false); } OBSBasicSettings::~OBSBasicSettings() { + bool disableHotkeysInFocus = config_get_bool(App()->GlobalConfig(), + "General", "DisableHotkeysInFocus"); + delete ui->filenameFormatting->completer(); main->EnableOutputs(true); + App()->EnableInFocusHotkeys(!disableHotkeysInFocus); } void OBSBasicSettings::SaveCombo(QComboBox *widget, const char *section, @@ -1092,30 +1108,37 @@ void OBSBasicSettings::LoadGeneralSettings() "BasicWindow", "StudioPortraitLayout"); ui->studioPortraitLayout->setChecked(studioPortraitLayout); + bool multiviewMouseSwitch = config_get_bool(GetGlobalConfig(), + "BasicWindow", "MultiviewMouseSwitch"); + ui->multiviewMouseSwitch->setChecked(multiviewMouseSwitch); + + bool multiviewDrawNames = config_get_bool(GetGlobalConfig(), + "BasicWindow", "MultiviewDrawNames"); + ui->multiviewDrawNames->setChecked(multiviewDrawNames); + + bool multiviewDrawAreas = config_get_bool(GetGlobalConfig(), + "BasicWindow", "MultiviewDrawAreas"); + ui->multiviewDrawAreas->setChecked(multiviewDrawAreas); + ui->multiviewLayout->addItem(QTStr( "Basic.Settings.General.MultiviewLayout.Horizontal.Top"), - QT_UTF8("horizontaltop")); + static_cast(MultiviewLayout::HORIZONTAL_TOP_8_SCENES)); ui->multiviewLayout->addItem(QTStr( "Basic.Settings.General.MultiviewLayout.Horizontal.Bottom"), - QT_UTF8("horizontalbottom")); + static_cast(MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES)); ui->multiviewLayout->addItem(QTStr( "Basic.Settings.General.MultiviewLayout.Vertical.Left"), - QT_UTF8("verticalleft")); + static_cast(MultiviewLayout::VERTICAL_LEFT_8_SCENES)); ui->multiviewLayout->addItem(QTStr( "Basic.Settings.General.MultiviewLayout.Vertical.Right"), - QT_UTF8("verticalright")); + static_cast(MultiviewLayout::VERTICAL_RIGHT_8_SCENES)); + ui->multiviewLayout->addItem(QTStr( + "Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top"), + static_cast(MultiviewLayout::HORIZONTAL_TOP_24_SCENES)); - const char *multiviewLayoutText = config_get_string(GetGlobalConfig(), - "BasicWindow", "MultiviewLayout"); - - if (astrcmpi(multiviewLayoutText, "horizontalbottom") == 0) - ui->multiviewLayout->setCurrentIndex(1); - else if (astrcmpi(multiviewLayoutText, "verticalleft") == 0) - ui->multiviewLayout->setCurrentIndex(2); - else if (astrcmpi(multiviewLayoutText, "verticalright") == 0) - ui->multiviewLayout->setCurrentIndex(3); - else - ui->multiviewLayout->setCurrentIndex(0); + ui->multiviewLayout->setCurrentIndex( + config_get_int(GetGlobalConfig(), "BasicWindow", + "MultiviewLayout")); loading = false; } @@ -2124,6 +2147,8 @@ void OBSBasicSettings::LoadAudioSettings() "ChannelSetup"); double meterDecayRate = config_get_double(main->Config(), "Audio", "MeterDecayRate"); + uint32_t peakMeterTypeIdx = config_get_uint(main->Config(), "Audio", + "PeakMeterType"); loading = true; @@ -2159,6 +2184,8 @@ void OBSBasicSettings::LoadAudioSettings() else ui->meterDecayRate->setCurrentIndex(0); + ui->peakMeterType->setCurrentIndex(peakMeterTypeIdx); + LoadAudioDevices(); LoadAudioSources(); @@ -2273,8 +2300,16 @@ void OBSBasicSettings::LoadAdvancedSettings() ui->enableNewSocketLoop->setChecked(enableNewSocketLoop); ui->enableLowLatencyMode->setChecked(enableLowLatencyMode); + + bool browserHWAccel = config_get_bool(App()->GlobalConfig(), + "General", "BrowserHWAccel"); + ui->browserHWAccel->setChecked(browserHWAccel); #endif + bool disableFocusHotkeys = config_get_bool(App()->GlobalConfig(), + "General", "DisableHotkeysInFocus"); + ui->disableFocusHotkeys->setChecked(disableFocusHotkeys); + loading = false; } @@ -2696,13 +2731,37 @@ void OBSBasicSettings::SaveGeneralSettings() main->ResetUI(); } - if (WidgetChanged(ui->multiviewLayout)) { - config_set_string(GetGlobalConfig(), "BasicWindow", - "MultiviewLayout", - QT_TO_UTF8(GetComboData(ui->multiviewLayout))); - - OBSProjector::UpdateMultiviewProjectors(); + bool multiviewChanged = false; + if (WidgetChanged(ui->multiviewMouseSwitch)) { + config_set_bool(GetGlobalConfig(), "BasicWindow", + "MultiviewMouseSwitch", + ui->multiviewMouseSwitch->isChecked()); + multiviewChanged = true; } + + if (WidgetChanged(ui->multiviewDrawNames)) { + config_set_bool(GetGlobalConfig(), "BasicWindow", + "MultiviewDrawNames", + ui->multiviewDrawNames->isChecked()); + multiviewChanged = true; + } + + if (WidgetChanged(ui->multiviewDrawAreas)) { + config_set_bool(GetGlobalConfig(), "BasicWindow", + "MultiviewDrawAreas", + ui->multiviewDrawAreas->isChecked()); + multiviewChanged = true; + } + + if (WidgetChanged(ui->multiviewLayout)) { + config_set_int(GetGlobalConfig(), "BasicWindow", + "MultiviewLayout", + ui->multiviewLayout->currentData().toInt()); + multiviewChanged = true; + } + + if (multiviewChanged) + OBSProjector::UpdateMultiviewProjectors(); } void OBSBasicSettings::SaveStream1Settings() @@ -2782,8 +2841,16 @@ void OBSBasicSettings::SaveAdvancedSettings() SaveCheckBox(ui->enableNewSocketLoop, "Output", "NewSocketLoopEnable"); SaveCheckBox(ui->enableLowLatencyMode, "Output", "LowLatencyEnable"); + + bool browserHWAccel = ui->browserHWAccel->isChecked(); + config_set_bool(App()->GlobalConfig(), "General", + "BrowserHWAccel", browserHWAccel); #endif + bool disableFocusHotkeys = ui->disableFocusHotkeys->isChecked(); + config_set_bool(App()->GlobalConfig(), "General", + "DisableHotkeysInFocus", disableFocusHotkeys); + #ifdef __APPLE__ if (WidgetChanged(ui->disableOSXVSync)) { bool disable = ui->disableOSXVSync->isChecked(); @@ -3113,6 +3180,14 @@ void OBSBasicSettings::SaveAudioSettings() main->UpdateVolumeControlsDecayRate(); } + if (WidgetChanged(ui->peakMeterType)) { + uint32_t peakMeterTypeIdx = ui->peakMeterType->currentIndex(); + config_set_uint(main->Config(), "Audio", "PeakMeterType", + peakMeterTypeIdx); + + main->UpdateVolumeControlsPeakMeterType(); + } + for (auto &audioSource : audioSources) { auto source = OBSGetStrongRef(get<0>(audioSource)); if (!source) @@ -4123,6 +4198,9 @@ void OBSBasicSettings::AdvReplayBufferChanged() int vbitrate = (int)obs_data_get_int(settings, "bitrate"); const char *rateControl = obs_data_get_string(settings, "rate_control"); + if (!rateControl) + rateControl = ""; + bool lossless = strcmp(rateControl, "lossless") == 0 || ui->advOutRecType->currentIndex() == 1; bool replayBufferEnabled = ui->advReplayBuf->isChecked(); @@ -4148,9 +4226,6 @@ void OBSBasicSettings::AdvReplayBufferChanged() if (memMB < 1) memMB = 1; - if (!rateControl) - rateControl = ""; - bool varRateControl = (astrcmpi(rateControl, "CBR") == 0 || astrcmpi(rateControl, "VBR") == 0 || astrcmpi(rateControl, "ABR") == 0); @@ -4244,12 +4319,6 @@ void OBSBasicSettings::SimpleRecordingEncoderChanged() warning += "\n\n"; warning += SIMPLE_OUTPUT_WARNING("Encoder"); } - - if (streamEnc == enc && enc == SIMPLE_ENCODER_QSV) { - if (!warning.isEmpty()) - warning += "\n\n"; - warning += SIMPLE_OUTPUT_WARNING("MultipleQSV"); - } } if (ui->simpleOutRecFormat->currentText().compare("mp4") == 0) { diff --git a/UI/window-basic-source-select.cpp b/UI/window-basic-source-select.cpp index 72b3558..da81782 100644 --- a/UI/window-basic-source-select.cpp +++ b/UI/window-basic-source-select.cpp @@ -38,6 +38,25 @@ bool OBSBasicSourceSelect::EnumSources(void *data, obs_source_t *source) return true; } +bool OBSBasicSourceSelect::EnumGroups(void *data, obs_source_t *source) +{ + OBSBasicSourceSelect *window = static_cast(data); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + + if (strcmp(id, window->id) == 0) { + OBSBasic *main = reinterpret_cast( + App()->GetMainWindow()); + OBSScene scene = main->GetCurrentScene(); + + obs_sceneitem_t *existing = obs_scene_get_group(scene, name); + if (!existing) + window->ui->sourceList->addItem(QT_UTF8(name)); + } + + return true; +} + void OBSBasicSourceSelect::OBSSourceAdded(void *data, calldata_t *calldata) { OBSBasicSourceSelect *window = static_cast(data); @@ -277,6 +296,8 @@ OBSBasicSourceSelect::OBSBasicSourceSelect(OBSBasic *parent, const char *id_) const char *name = obs_source_get_name(sceneSource); ui->sourceList->addItem(QT_UTF8(name)); } + } else if (strcmp(id_, "group") == 0) { + obs_enum_sources(EnumGroups, this); } else { obs_enum_sources(EnumSources, this); } diff --git a/UI/window-basic-source-select.hpp b/UI/window-basic-source-select.hpp index 7bab476..9049679 100644 --- a/UI/window-basic-source-select.hpp +++ b/UI/window-basic-source-select.hpp @@ -32,6 +32,7 @@ private: const char *id; static bool EnumSources(void *data, obs_source_t *source); + static bool EnumGroups(void *data, obs_source_t *source); static void OBSSourceRemoved(void *data, calldata_t *calldata); static void OBSSourceAdded(void *data, calldata_t *calldata); diff --git a/UI/window-basic-stats.cpp b/UI/window-basic-stats.cpp index 2a66c2d..b758c2f 100644 --- a/UI/window-basic-stats.cpp +++ b/UI/window-basic-stats.cpp @@ -136,6 +136,7 @@ OBSBasicStats::OBSBasicStats(QWidget *parent) Qt::WindowMinimizeButtonHint | Qt::WindowCloseButtonHint); setWindowTitle(QTStr("Basic.Stats")); + setWindowIcon(QIcon(":/res/images/obs.png")); setWindowModality(Qt::NonModal); setAttribute(Qt::WA_DeleteOnClose, true); diff --git a/UI/window-basic-transform.cpp b/UI/window-basic-transform.cpp index 7b0b4b0..1f529dd 100644 --- a/UI/window-basic-transform.cpp +++ b/UI/window-basic-transform.cpp @@ -4,23 +4,28 @@ Q_DECLARE_METATYPE(OBSSceneItem); -static OBSSceneItem FindASelectedItem(OBSScene scene) +static bool find_sel(obs_scene_t *, obs_sceneitem_t *item, void *param) { - auto func = [] (obs_scene_t *scene, obs_sceneitem_t *item, void *param) - { - OBSSceneItem &dst = *reinterpret_cast(param); + OBSSceneItem &dst = *reinterpret_cast(param); - if (obs_sceneitem_selected(item)) { - dst = item; + if (obs_sceneitem_selected(item)) { + dst = item; + return false; + } + if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, find_sel, param); + if (!!dst) { return false; } + } - UNUSED_PARAMETER(scene); - return true; - }; + return true; +}; +static OBSSceneItem FindASelectedItem(OBSScene scene) +{ OBSSceneItem item; - obs_scene_enum_items(scene, func, &item); + obs_scene_enum_items(scene, find_sel, &item); return item; } @@ -63,9 +68,10 @@ OBSBasicTransform::OBSBasicTransform(OBSBasic *parent) installEventFilter(CreateShortcutFilter()); - OBSScene curScene = main->GetCurrentScene(); - SetScene(curScene); - SetItem(FindASelectedItem(curScene)); + OBSSceneItem item = FindASelectedItem(main->GetCurrentScene()); + OBSScene scene = obs_sceneitem_get_scene(item); + SetScene(scene); + SetItem(item); channelChangedSignal.Connect(obs_get_signal_handler(), "channel_change", OBSChannelChanged, this); diff --git a/UI/window-projector.cpp b/UI/window-projector.cpp index 0ad9755..2ee09f7 100644 --- a/UI/window-projector.cpp +++ b/UI/window-projector.cpp @@ -3,30 +3,56 @@ #include #include #include -#include "window-projector.hpp" +#include "obs-app.hpp" +#include "window-basic-main.hpp" #include "display-helpers.hpp" #include "qt-wrappers.hpp" #include "platform.hpp" -#define HORIZONTAL_TOP 0 -#define HORIZONTAL_BOTTOM 1 -#define VERTICAL_LEFT 2 -#define VERTICAL_RIGHT 3 - +static QList windowedProjectors; static QList multiviewProjectors; -static bool updatingMultiview = false; -static int multiviewLayout = HORIZONTAL_TOP; +static bool updatingMultiview = false, drawLabel, drawSafeArea, mouseSwitching, + transitionOnDoubleClick; +static MultiviewLayout multiviewLayout; +static size_t maxSrcs, numSrcs; -OBSProjector::OBSProjector(QWidget *widget, obs_source_t *source_, bool window) +OBSProjector::OBSProjector(QWidget *widget, obs_source_t *source_, int monitor, + QString title, ProjectorType type_) : OBSQTDisplay (widget, Qt::Window), source (source_), removedSignal (obs_source_get_signal_handler(source), "remove", OBSSourceRemoved, this) { - if (!window) { + projectorTitle = std::move(title); + savedMonitor = monitor; + isWindow = savedMonitor < 0; + type = type_; + + if (isWindow) { + setWindowIcon(QIcon(":/res/images/obs.png")); + + UpdateProjectorTitle(projectorTitle); + windowedProjectors.push_back(this); + + resize(480, 270); + + SetAlwaysOnTop(this, config_get_bool(GetGlobalConfig(), + "BasicWindow", "ProjectorAlwaysOnTop")); + } else { setWindowFlags(Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint); + + QScreen *screen = QGuiApplication::screens()[savedMonitor]; + setGeometry(screen->geometry()); + + QAction *action = new QAction(this); + action->setShortcut(Qt::Key_Escape); + addAction(action); + connect(action, SIGNAL(triggered()), this, + SLOT(EscapeTriggered())); + + SetAlwaysOnTop(this, true); } setAttribute(Qt::WA_DeleteOnClose, true); @@ -49,14 +75,82 @@ OBSProjector::OBSProjector(QWidget *widget, obs_source_t *source_, bool window) bool hideCursor = config_get_bool(GetGlobalConfig(), "BasicWindow", "HideProjectorCursor"); - if (hideCursor && !window) { + if (hideCursor && !isWindow) { QPixmap empty(16, 16); empty.fill(Qt::transparent); setCursor(QCursor(empty)); } + if (type == ProjectorType::Multiview) { + obs_enter_graphics(); + + // All essential action should be placed inside this area + gs_render_start(true); + gs_vertex2f(actionSafePercentage, actionSafePercentage); + gs_vertex2f(actionSafePercentage, 1 - actionSafePercentage); + gs_vertex2f(1 - actionSafePercentage, 1 - actionSafePercentage); + gs_vertex2f(1 - actionSafePercentage, actionSafePercentage); + gs_vertex2f(actionSafePercentage, actionSafePercentage); + actionSafeMargin = gs_render_save(); + + // All graphics should be placed inside this area + gs_render_start(true); + gs_vertex2f(graphicsSafePercentage, graphicsSafePercentage); + gs_vertex2f(graphicsSafePercentage, 1 - graphicsSafePercentage); + gs_vertex2f(1 - graphicsSafePercentage, + 1 - graphicsSafePercentage); + gs_vertex2f(1 - graphicsSafePercentage, graphicsSafePercentage); + gs_vertex2f(graphicsSafePercentage, graphicsSafePercentage); + graphicsSafeMargin = gs_render_save(); + + // 4:3 safe area for widescreen + gs_render_start(true); + gs_vertex2f(fourByThreeSafePercentage, graphicsSafePercentage); + gs_vertex2f(1 - fourByThreeSafePercentage, + graphicsSafePercentage); + gs_vertex2f(1 - fourByThreeSafePercentage, 1 - + graphicsSafePercentage); + gs_vertex2f(fourByThreeSafePercentage, + 1 - graphicsSafePercentage); + gs_vertex2f(fourByThreeSafePercentage, graphicsSafePercentage); + fourByThreeSafeMargin = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.5f); + gs_vertex2f(lineLength, 0.5f); + leftLine = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.5f, 0.0f); + gs_vertex2f(0.5f, lineLength); + topLine = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.5f); + gs_vertex2f(1 - lineLength, 0.5f); + rightLine = gs_render_save(); + obs_leave_graphics(); + + solid = obs_get_base_effect(OBS_EFFECT_SOLID); + color = gs_effect_get_param_by_name(solid, "color"); + + UpdateMultiview(); + + multiviewProjectors.push_back(this); + } + App()->IncrementSleepInhibition(); - resize(480, 270); + + if (source) + obs_source_inc_showing(source); + + ready = true; + + show(); + + // We need it here to allow keyboard input in X11 to listen to Escape + if (!isWindow) + activateWindow(); } OBSProjector::~OBSProjector() @@ -76,10 +170,9 @@ OBSProjector::~OBSProjector() } obs_enter_graphics(); - gs_vertexbuffer_destroy(outerBox); - gs_vertexbuffer_destroy(innerBox); - gs_vertexbuffer_destroy(leftVLine); - gs_vertexbuffer_destroy(rightVLine); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); gs_vertexbuffer_destroy(leftLine); gs_vertexbuffer_destroy(topLine); gs_vertexbuffer_destroy(rightLine); @@ -89,6 +182,9 @@ OBSProjector::~OBSProjector() if (type == ProjectorType::Multiview) multiviewProjectors.removeAll(this); + if (isWindow) + windowedProjectors.removeAll(this); + App()->DecrementSleepInhibition(); } @@ -132,92 +228,6 @@ static OBSSource CreateLabel(const char *name, size_t h) return txtSource; } -void OBSProjector::Init(int monitor, bool window, QString title, - ProjectorType type_) -{ - QScreen *screen = QGuiApplication::screens()[monitor]; - - if (!window) - setGeometry(screen->geometry()); - - bool alwaysOnTop = config_get_bool(GetGlobalConfig(), - "BasicWindow", "ProjectorAlwaysOnTop"); - if (alwaysOnTop && !window) - SetAlwaysOnTop(this, true); - - if (window) - setWindowTitle(title); - - show(); - - if (source) - obs_source_inc_showing(source); - - if (!window) { - QAction *action = new QAction(this); - action->setShortcut(Qt::Key_Escape); - addAction(action); - connect(action, SIGNAL(triggered()), this, - SLOT(EscapeTriggered())); - activateWindow(); - } - - savedMonitor = monitor; - isWindow = window; - type = type_; - - if (type == ProjectorType::Multiview) { - obs_enter_graphics(); - gs_render_start(true); - gs_vertex2f(0.001f, 0.001f); - gs_vertex2f(0.001f, 0.997f); - gs_vertex2f(0.997f, 0.997f); - gs_vertex2f(0.997f, 0.001f); - gs_vertex2f(0.001f, 0.001f); - outerBox = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.04f, 0.04f); - gs_vertex2f(0.04f, 0.96f); - gs_vertex2f(0.96f, 0.96f); - gs_vertex2f(0.96f, 0.04f); - gs_vertex2f(0.04f, 0.04f); - innerBox = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.15f, 0.04f); - gs_vertex2f(0.15f, 0.96f); - leftVLine = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.85f, 0.04f); - gs_vertex2f(0.85f, 0.96f); - rightVLine = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.5f); - gs_vertex2f(0.075f, 0.5f); - leftLine = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.5f, 0.0f); - gs_vertex2f(0.5f, 0.09f); - topLine = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.925f, 0.5f); - gs_vertex2f(1.0f, 0.5f); - rightLine = gs_render_save(); - obs_leave_graphics(); - - UpdateMultiview(); - - multiviewProjectors.push_back(this); - } - - ready = true; -} - static inline void renderVB(gs_effect_t *effect, gs_vertbuffer_t *vb, int cx, int cy) { @@ -243,10 +253,36 @@ static inline void renderVB(gs_effect_t *effect, gs_vertbuffer_t *vb, static inline uint32_t labelOffset(obs_source_t *label, uint32_t cx) { uint32_t w = obs_source_get_width(label); - w = uint32_t(float(w) * 0.5f); + + int n; // Number of scenes per row + switch (multiviewLayout) { + case MultiviewLayout::HORIZONTAL_TOP_24_SCENES: + n = 6; + break; + default: + n = 4; + break; + } + + w = uint32_t(w * ((1.0f) / n)); return (cx / 2) - w; } +static inline void startRegion(int vX, int vY, int vCX, int vCY, float oL, + float oR, float oT, float oB) +{ + gs_projection_push(); + gs_viewport_push(); + gs_set_viewport(vX, vY, vCX, vCY); + gs_ortho(oL, oR, oT, oB, -100.0f, 100.0f); +} + +static inline void endRegion() +{ + gs_viewport_pop(); + gs_projection_pop(); +} + void OBSProjector::OBSRenderMultiview(void *data, uint32_t cx, uint32_t cy) { OBSProjector *window = (OBSProjector *)data; @@ -257,327 +293,342 @@ void OBSProjector::OBSRenderMultiview(void *data, uint32_t cx, uint32_t cy) OBSBasic *main = (OBSBasic *)obs_frontend_get_main_window(); uint32_t targetCX, targetCY; int x, y; - float fX, fY, halfCX, halfCY, sourceX, sourceY, labelX, labelY, - quarterCX, quarterCY, scale, targetCXF, targetCYF, - hiCX, hiCY, qiX, qiY, qiCX, qiCY, hiScaleX, hiScaleY, - qiScaleX, qiScaleY; - uint32_t offset; + float scale; - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - targetCX = ovi.base_width; - targetCY = ovi.base_height; + targetCX = (uint32_t)window->fw; + targetCY = (uint32_t)window->fh; GetScaleAndCenterPos(targetCX, targetCY, cx, cy, x, y, scale); - targetCXF = float(targetCX); - targetCYF = float(targetCY); - fX = float(x); - fY = float(y); - - halfCX = (targetCXF + 1) / 2; - halfCY = (targetCYF + 1) / 2; - hiCX = (halfCX - 4.0); - hiCY = (halfCY - 4.0); - hiScaleX = hiCX / targetCXF; - hiScaleY = hiCY / targetCYF; - - quarterCX = (halfCX + 1) / 2; - quarterCY = (halfCY + 1) / 2; - qiCX = (quarterCX - 8.0); - qiCY = (quarterCY - 8.0); - qiScaleX = qiCX / targetCXF; - qiScaleY = qiCY / targetCYF; - OBSSource previewSrc = main->GetCurrentSceneSource(); OBSSource programSrc = main->GetProgramSource(); - bool studioMode = main->IsPreviewProgramMode(); - auto drawBox = [solid, color] (float cx, float cy, + auto renderVB = [&](gs_vertbuffer_t *vb, int cx, int cy, uint32_t colorVal) { - gs_effect_set_color(color, colorVal); - while (gs_effect_loop(solid, "Solid")) + if (!vb) + return; + + matrix4 transform; + matrix4_identity(&transform); + transform.x.x = cx; + transform.y.y = cy; + + gs_load_vertexbuffer(vb); + + gs_matrix_push(); + gs_matrix_mul(&transform); + + gs_effect_set_color(window->color, colorVal); + while (gs_effect_loop(window->solid, "Solid")) + gs_draw(GS_LINESTRIP, 0, 0); + + gs_matrix_pop(); + }; + + auto drawBox = [&](float cx, float cy, uint32_t colorVal) + { + gs_effect_set_color(window->color, colorVal); + while (gs_effect_loop(window->solid, "Solid")) gs_draw_sprite(nullptr, 0, (uint32_t)cx, (uint32_t)cy); }; - auto setRegion = [fX, fY, scale] (float x, float y, float cx, float cy) + auto setRegion = [&](float bx, float by, float cx, + float cy) { - float vX = int(fX + x * scale); - float vY = int(fY + y * scale); + float vX = int(x + bx * scale); + float vY = int(y + by * scale); float vCX = int(cx * scale); float vCY = int(cy * scale); - float oL = x; - float oT = y; - float oR = (x + cx); - float oB = (y + cy); + float oL = bx; + float oT = by; + float oR = (bx + cx); + float oB = (by + cy); - gs_projection_push(); - gs_viewport_push(); - gs_set_viewport(vX, vY, vCX, vCY); - gs_ortho(oL, oR, oT, oB, -100.0f, 100.0f); - }; - - auto resetRegion = [] () - { - gs_viewport_pop(); - gs_projection_pop(); + startRegion(vX, vY, vCX, vCY, oL, oR, oT, oB); }; auto calcBaseSource = [&](size_t i) { switch (multiviewLayout) { - case VERTICAL_LEFT: - sourceX = halfCX; - sourceY = (i / 2 ) * quarterCY; - if (i % 2 != 0) - sourceX = halfCX + quarterCX; + case MultiviewLayout::HORIZONTAL_TOP_24_SCENES: + window->sourceX = (i % 6) * window->scenesCX; + window->sourceY = window->pvwprgCY + + (i / 6) * window->scenesCY; break; - case VERTICAL_RIGHT: - sourceX = 0; - sourceY = (i / 2 ) * quarterCY; + case MultiviewLayout::VERTICAL_LEFT_8_SCENES: + window->sourceX = window->pvwprgCX; + window->sourceY = (i / 2 ) * window->scenesCY; if (i % 2 != 0) - sourceX = quarterCX; + window->sourceX += window->scenesCX; break; - case HORIZONTAL_BOTTOM: + case MultiviewLayout::VERTICAL_RIGHT_8_SCENES: + window->sourceX = 0; + window->sourceY = (i / 2 ) * window->scenesCY; + if (i % 2 != 0) + window->sourceX = window->scenesCX; + break; + case MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES: if (i < 4) { - sourceX = (float(i) * quarterCX); - sourceY = 0; + window->sourceX = (float(i) * window->scenesCX); + window->sourceY = 0; } else { - sourceX = (float(i - 4) * quarterCX); - sourceY = quarterCY; + window->sourceX = (float(i - 4) * + window->scenesCX); + window->sourceY = window->scenesCY; } break; - default: //HORIZONTAL_TOP: + default: // MultiviewLayout::HORIZONTAL_TOP_8_SCENES: if (i < 4) { - sourceX = (float(i) * quarterCX); - sourceY = halfCY; + window->sourceX = (float(i) * window->scenesCX); + window->sourceY = window->pvwprgCY; } else { - sourceX = (float(i - 4) * quarterCX); - sourceY = halfCY + quarterCY; + window->sourceX = (float(i - 4) * + window->scenesCX); + window->sourceY = window->pvwprgCY + + window->scenesCY; } } + window->siX = window->sourceX + window->thickness; + window->siY = window->sourceY + window->thickness; }; auto calcPreviewProgram = [&](bool program) { switch (multiviewLayout) { - case VERTICAL_LEFT: - sourceX = 2.0f; - sourceY = halfCY + 2.0f; - labelX = offset; - labelY = halfCY * 1.8f; + case MultiviewLayout::HORIZONTAL_TOP_24_SCENES: + window->sourceX = window->thickness + + window->pvwprgCX / 2; + window->sourceY = window->thickness; + window->labelX = window->offset + window->pvwprgCX / 2; + window->labelY = window->pvwprgCY * 0.85f; if (program) { - sourceY = 2.0f; - labelY = halfCY * 0.8f; + window->sourceX += window->pvwprgCX; + window->labelX += window->pvwprgCX; } break; - case VERTICAL_RIGHT: - sourceX = halfCX + 2.0f; - sourceY = halfCY + 2.0f; - labelX = halfCX + offset; - labelY = halfCY * 1.8f; + case MultiviewLayout::VERTICAL_LEFT_8_SCENES: + window->sourceX = window->thickness; + window->sourceY = window->pvwprgCY + window->thickness; + window->labelX = window->offset; + window->labelY = window->pvwprgCY * 1.85f; if (program) { - sourceY = 2.0f; - labelY = halfCY * 0.8f; + window->sourceY = window->thickness; + window->labelY = window->pvwprgCY * 0.85f; } break; - case HORIZONTAL_BOTTOM: - sourceX = 2.0f; - sourceY = halfCY + 2.0f; - labelX = offset; - labelY = halfCY * 1.8f; + case MultiviewLayout::VERTICAL_RIGHT_8_SCENES: + window->sourceX = window->pvwprgCX + window->thickness; + window->sourceY = window->pvwprgCY + window->thickness; + window->labelX = window->pvwprgCX + window->offset; + window->labelY = window->pvwprgCY * 1.85f; if (program) { - sourceX = halfCX + 2.0f; - labelX = halfCX + offset; + window->sourceY = window->thickness; + window->labelY = window->pvwprgCY * 0.85f; } break; - default: //HORIZONTAL_TOP: - sourceX = 2.0f; - sourceY = 2.0f; - labelX = offset; - labelY = halfCY * 0.8f; + case MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES: + window->sourceX = window->thickness; + window->sourceY = window->pvwprgCY + window->thickness; + window->labelX = window->offset; + window->labelY = window->pvwprgCY * 1.85f; if (program) { - sourceX = halfCX + 2.0f; - labelX = halfCX + offset; + window->sourceX += window->pvwprgCX; + window->labelX += window->pvwprgCX; + } + break; + default: // MultiviewLayout::HORIZONTAL_TOP_8_SCENES: + window->sourceX = window->thickness; + window->sourceY = window->thickness; + window->labelX = window->offset; + window->labelY = window->pvwprgCY * 0.85f; + if (program) { + window->sourceX += window->pvwprgCX; + window->labelX += window->pvwprgCX; } } }; + auto paintAreaWithColor = [&](float tx, float ty, float cx, float cy, + uint32_t color) + { + gs_matrix_push(); + gs_matrix_translate3f(tx, ty, 0.0f); + drawBox(cx, cy, color); + gs_matrix_pop(); + }; + + // Define the whole usable region for the multiview + startRegion(x, y, targetCX * scale, targetCY * scale, 0.0f, window->fw, + 0.0f, window->fh); + + // Change the background color to highlight all sources + drawBox(window->fw, window->fh, outerColor); + /* ----------------------------- */ /* draw sources */ - gs_projection_push(); - gs_viewport_push(); - gs_set_viewport(x, y, targetCX * scale, targetCY * scale); - gs_ortho(0.0f, targetCXF, 0.0f, targetCYF, -100.0f, 100.0f); + for (size_t i = 0; i < maxSrcs; i++) { + // Handle all the offsets + calcBaseSource(i); - for (size_t i = 0; i < 8; i++) { - OBSSource src = OBSGetStrongRef(window->multiviewScenes[i]); - obs_source *label = window->multiviewLabels[i + 2]; - - if (!src) + if (i >= numSrcs) { + // Just paint the background and continue + paintAreaWithColor(window->sourceX, window->sourceY, + window->scenesCX, window->scenesCY, + outerColor); + paintAreaWithColor(window->siX, window->siY, + window->siCX, window->siCY, + backgroundColor); continue; + } + + OBSSource src = OBSGetStrongRef(window->multiviewScenes[i]); + + // We have a source. Now chose the proper highlight color + uint32_t colorVal = outerColor; + if (src == programSrc) + colorVal = programColor; + else if (src == previewSrc) + colorVal = studioMode ? previewColor : programColor; + + // Paint the background + paintAreaWithColor(window->sourceX, window->sourceY, + window->scenesCX, window->scenesCY, colorVal); + paintAreaWithColor(window->siX, window->siY, window->siCX, + window->siCY, backgroundColor); + + /* ----------- */ + + // Render the source + gs_matrix_push(); + gs_matrix_translate3f(window->siX, window->siY, 0.0f); + gs_matrix_scale3f(window->siScaleX, window->siScaleY, 1.0f); + setRegion(window->siX, window->siY, window->siCX, window->siCY); + obs_source_video_render(src); + endRegion(); + gs_matrix_pop(); + + /* ----------- */ + + // Render the label + if (!drawLabel) + continue; + + obs_source *label = window->multiviewLabels[i + 2]; if (!label) continue; - calcBaseSource(i); - - qiX = sourceX + 4.0f; - qiY = sourceY + 4.0f; - - /* ----------- */ - - if (src == previewSrc || src == programSrc) { - uint32_t colorVal = src == programSrc - ? 0xFFFF0000 - : 0xFF00FF00; - - gs_matrix_push(); - gs_matrix_translate3f(sourceX, sourceY, 0.0f); - drawBox(quarterCX, quarterCY, colorVal); - gs_matrix_pop(); - - gs_matrix_push(); - gs_matrix_translate3f(qiX, qiY, 0.0f); - drawBox(qiCX, qiCY, 0xFF000000); - gs_matrix_pop(); - } - - /* ----------- */ + window->offset = labelOffset(label, window->scenesCX); gs_matrix_push(); - gs_matrix_translate3f(qiX, qiY, 0.0f); - gs_matrix_scale3f(qiScaleX, qiScaleY, 1.0f); - - setRegion(qiX, qiY, qiCX, qiCY); - obs_source_video_render(src); - resetRegion(); - - gs_effect_set_color(color, 0xFFFFFFFF); - renderVB(solid, window->outerBox, targetCX, targetCY); - - gs_matrix_pop(); - - /* ----------- */ - - offset = labelOffset(label, quarterCX); - cx = obs_source_get_width(label); - cy = obs_source_get_height(label); - - gs_matrix_push(); - gs_matrix_translate3f(sourceX + offset, - (quarterCY * 0.8f) + sourceY, 0.0f); - - drawBox(cx, cy + int(quarterCX * 0.015f), 0xD91F1F1F); + gs_matrix_translate3f(window->sourceX + window->offset, + (window->scenesCY * 0.85f) + window->sourceY, + 0.0f); + gs_matrix_scale3f(window->ppiScaleX, window->ppiScaleY, 1.0f); + drawBox(obs_source_get_width(label), + obs_source_get_height(label) + + int(window->sourceY * 0.015f), labelColor); obs_source_video_render(label); - gs_matrix_pop(); } - gs_effect_set_color(color, 0xFFFFFFFF); - /* ----------------------------- */ /* draw preview */ obs_source_t *previewLabel = window->multiviewLabels[0]; - offset = labelOffset(previewLabel, halfCX); + window->offset = labelOffset(previewLabel, window->pvwprgCX); calcPreviewProgram(false); + // Paint the background + paintAreaWithColor(window->sourceX, window->sourceY, window->ppiCX, + window->ppiCY, backgroundColor); + + // Scale and Draw the preview gs_matrix_push(); - gs_matrix_translate3f(sourceX, sourceY, 0.0f); - gs_matrix_scale3f(hiScaleX, hiScaleY, 1.0f); - - setRegion(sourceX, sourceY, hiCX, hiCY); - - if (studioMode) { + gs_matrix_translate3f(window->sourceX, window->sourceY, 0.0f); + gs_matrix_scale3f(window->ppiScaleX, window->ppiScaleY, 1.0f); + setRegion(window->sourceX, window->sourceY, window->ppiCX, + window->ppiCY); + if (studioMode) obs_source_video_render(previewSrc); - } else { + else obs_render_main_texture(); + if (drawSafeArea) { + renderVB(window->actionSafeMargin, targetCX, targetCY, + outerColor); + renderVB(window->graphicsSafeMargin, targetCX, targetCY, + outerColor); + renderVB(window->fourByThreeSafeMargin, targetCX, targetCY, + outerColor); + renderVB(window->leftLine, targetCX, targetCY, outerColor); + renderVB(window->topLine, targetCX, targetCY, outerColor); + renderVB(window->rightLine, targetCX, targetCY, outerColor); } - - resetRegion(); - + endRegion(); gs_matrix_pop(); /* ----------- */ - gs_matrix_push(); - gs_matrix_translate3f(sourceX, sourceY, 0.0f); - gs_matrix_scale3f(hiScaleX, hiScaleY, 1.0f); - - renderVB(solid, window->outerBox, targetCX, targetCY); - renderVB(solid, window->innerBox, targetCX, targetCY); - renderVB(solid, window->leftVLine, targetCX, targetCY); - renderVB(solid, window->rightVLine, targetCX, targetCY); - renderVB(solid, window->leftLine, targetCX, targetCY); - renderVB(solid, window->topLine, targetCX, targetCY); - renderVB(solid, window->rightLine, targetCX, targetCY); - - gs_matrix_pop(); - - /* ----------- */ - - cx = obs_source_get_width(previewLabel); - cy = obs_source_get_height(previewLabel); - - gs_matrix_push(); - gs_matrix_translate3f(labelX, labelY, 0.0f); - - drawBox(cx, cy + int(halfCX * 0.015f), 0xD91F1F1F); - obs_source_video_render(previewLabel); - - gs_matrix_pop(); + // Draw the Label + if (drawLabel) { + gs_matrix_push(); + gs_matrix_translate3f(window->labelX, window->labelY, 0.0f); + gs_matrix_scale3f(window->ppiScaleX, window->ppiScaleY, 1.0f); + drawBox(obs_source_get_width(previewLabel), + obs_source_get_height(previewLabel) + + int(window->pvwprgCX * 0.015f), labelColor); + obs_source_video_render(previewLabel); + gs_matrix_pop(); + } /* ----------------------------- */ /* draw program */ obs_source_t *programLabel = window->multiviewLabels[1]; - offset = labelOffset(programLabel, halfCX); + window->offset = labelOffset(programLabel, window->pvwprgCX); calcPreviewProgram(true); + // Scale and Draw the program gs_matrix_push(); - gs_matrix_translate3f(sourceX, sourceY, 0.0f); - gs_matrix_scale3f(hiScaleX, hiScaleY, 1.0f); - - setRegion(sourceX, sourceY, hiCX, hiCY); + gs_matrix_translate3f(window->sourceX, window->sourceY, 0.0f); + gs_matrix_scale3f(window->ppiScaleX, window->ppiScaleY, 1.0f); + setRegion(window->sourceX, window->sourceY, window->ppiCX, + window->ppiCY); obs_render_main_texture(); - resetRegion(); - + endRegion(); gs_matrix_pop(); /* ----------- */ - gs_matrix_push(); - gs_matrix_translate3f(sourceX, sourceY, 0.0f); - gs_matrix_scale3f(hiScaleX, hiScaleY, 1.0f); + // Draw the Label + if (drawLabel) { + gs_matrix_push(); + gs_matrix_translate3f(window->labelX, window->labelY, 0.0f); + gs_matrix_scale3f(window->ppiScaleX, window->ppiScaleY, 1.0f); + drawBox(obs_source_get_width(programLabel), + obs_source_get_height(programLabel) + + int(window->pvwprgCX * 0.015f), labelColor); + obs_source_video_render(programLabel); + gs_matrix_pop(); + } - renderVB(solid, window->outerBox, targetCX, targetCY); + // Region for future usage with aditional info. + if (multiviewLayout == MultiviewLayout::HORIZONTAL_TOP_24_SCENES) { + // Just paint the background for now + paintAreaWithColor(window->thickness, window->thickness, + window->siCX, window->siCY * 2 + + window->thicknessx2, backgroundColor); + paintAreaWithColor(window->thickness + 2.5 * ( + window->thicknessx2 + window->ppiCX), + window->thickness, window->siCX, + window->siCY * 2 + window->thicknessx2, + backgroundColor); + } - gs_matrix_pop(); - - /* ----------- */ - - cx = obs_source_get_width(programLabel); - cy = obs_source_get_height(programLabel); - - gs_matrix_push(); - gs_matrix_translate3f(labelX, labelY, 0.0f); - - drawBox(cx, cy + int(halfCX * 0.015f), 0xD91F1F1F); - obs_source_video_render(programLabel); - - gs_matrix_pop(); - - /* ----------------------------- */ - - gs_viewport_pop(); - gs_projection_pop(); + endRegion(); } void OBSProjector::OBSRender(void *data, uint32_t cx, uint32_t cy) @@ -588,6 +639,7 @@ void OBSProjector::OBSRender(void *data, uint32_t cx, uint32_t cy) return; OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + OBSSource source = window->source; uint32_t targetCX; uint32_t targetCY; @@ -595,9 +647,9 @@ void OBSProjector::OBSRender(void *data, uint32_t cx, uint32_t cy) int newCX, newCY; float scale; - if (window->source) { - targetCX = std::max(obs_source_get_width(window->source), 1u); - targetCY = std::max(obs_source_get_height(window->source), 1u); + if (source) { + targetCX = std::max(obs_source_get_width(source), 1u); + targetCY = std::max(obs_source_get_height(source), 1u); } else { struct obs_video_info ovi; obs_get_video_info(&ovi); @@ -610,32 +662,26 @@ void OBSProjector::OBSRender(void *data, uint32_t cx, uint32_t cy) newCX = int(scale * float(targetCX)); newCY = int(scale * float(targetCY)); - gs_viewport_push(); - gs_projection_push(); - gs_ortho(0.0f, float(targetCX), 0.0f, float(targetCY), -100.0f, 100.0f); - gs_set_viewport(x, y, newCX, newCY); - - OBSSource source = window->source; + startRegion(x, y, newCX, newCY, 0.0f, float(targetCX), 0.0f, + float(targetCY)); if (window->type == ProjectorType::Preview && main->IsPreviewProgramMode()) { OBSSource curSource = main->GetCurrentSceneSource(); - if (window->source != curSource) { - obs_source_dec_showing(window->source); + if (source != curSource) { + obs_source_dec_showing(source); obs_source_inc_showing(curSource); source = curSource; } } - if (source) { + if (source) obs_source_video_render(source); - } else { + else obs_render_main_texture(); - } - gs_projection_pop(); - gs_viewport_pop(); + endRegion(); } void OBSProjector::OBSSourceRemoved(void *data, calldata_t *params) @@ -647,35 +693,50 @@ void OBSProjector::OBSSourceRemoved(void *data, calldata_t *params) UNUSED_PARAMETER(params); } -static int getSourceByPosition(int x, int y) +static int getSourceByPosition(int x, int y, float ratio) { - struct obs_video_info ovi; - obs_get_video_info(&ovi); - float ratio = float(ovi.base_width) / float(ovi.base_height); - - QWidget *rec = QApplication::activeWindow(); + int pos = -1; + QWidget *rec = QApplication::activeWindow(); + if (!rec) + return pos; int cx = rec->width(); int cy = rec->height(); int minX = 0; int minY = 0; int maxX = cx; int maxY = cy; - int halfX = cx / 2; - int halfY = cy / 2; - int pos = -1; switch (multiviewLayout) { - case VERTICAL_LEFT: + case MultiviewLayout::HORIZONTAL_TOP_24_SCENES: if (float(cx) / float(cy) > ratio) { int validX = cy * ratio; - maxX = halfX + (validX / 2); + minX = (cx / 2) - (validX / 2); + maxX = (cx / 2) + (validX / 2); + minY = cy / 3; } else { int validY = cx / ratio; - minY = halfY - (validY / 2); - maxY = halfY + (validY / 2); + maxY = (cy / 2) + (validY / 2); + minY = (cy / 2) - (validY / 6); } - minX = halfX; + if (x < minX || x > maxX || y < minY || y > maxY) + break; + + pos = (x - minX) / ((maxX - minX) / 6); + pos += ((y - minY) / ((maxY - minY) / 4)) * 6; + + break; + case MultiviewLayout::VERTICAL_LEFT_8_SCENES: + if (float(cx) / float(cy) > ratio) { + int validX = cy * ratio; + maxX = (cx / 2) + (validX / 2); + } else { + int validY = cx / ratio; + minY = (cy / 2) - (validY / 2); + maxY = (cy / 2) + (validY / 2); + } + + minX = cx / 2; if (x < minX || x > maxX || y < minY || y > maxY) break; @@ -684,17 +745,17 @@ static int getSourceByPosition(int x, int y) if (x > minX + ((maxX - minX) / 2)) pos++; break; - case VERTICAL_RIGHT: + case MultiviewLayout::VERTICAL_RIGHT_8_SCENES: if (float(cx) / float(cy) > ratio) { int validX = cy * ratio; - minX = halfX - (validX / 2); + minX = (cx / 2) - (validX / 2); } else { int validY = cx / ratio; - minY = halfY - (validY / 2); - maxY = halfY + (validY / 2); + minY = (cy / 2) - (validY / 2); + maxY = (cy / 2) + (validY / 2); } - maxX = halfX; + maxX = (cx / 2); if (x < minX || x > maxX || y < minY || y > maxY) break; @@ -703,17 +764,17 @@ static int getSourceByPosition(int x, int y) if (x > minX + ((maxX - minX) / 2)) pos++; break; - case HORIZONTAL_BOTTOM: + case MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES: if (float(cx) / float(cy) > ratio) { int validX = cy * ratio; - minX = halfX - (validX / 2); - maxX = halfX + (validX / 2); + minX = (cx / 2) - (validX / 2); + maxX = (cx / 2) + (validX / 2); } else { int validY = cx / ratio; - minY = halfY - (validY / 2); + minY = (cy / 2) - (validY / 2); } - maxY = halfY; + maxY = (cy / 2); if (x < minX || x > maxX || y < minY || y > maxY) break; @@ -722,17 +783,17 @@ static int getSourceByPosition(int x, int y) if (y > minY + ((maxY - minY) / 2)) pos += 4; break; - default: // HORIZONTAL_TOP + default: // MultiviewLayout::HORIZONTAL_TOP_8_SCENES if (float(cx) / float(cy) > ratio) { int validX = cy * ratio; - minX = halfX - (validX / 2); - maxX = halfX + (validX / 2); + minX = (cx / 2) - (validX / 2); + maxX = (cx / 2) + (validX / 2); } else { int validY = cx / ratio; - maxY = halfY + (validY / 2); + maxY = (cy / 2) + (validY / 2); } - minY = halfY; + minY = (cy / 2); if (x < minX || x > maxX || y < minY || y > maxY) break; @@ -749,8 +810,10 @@ void OBSProjector::mouseDoubleClickEvent(QMouseEvent *event) { OBSQTDisplay::mouseDoubleClickEvent(event); - if (!config_get_bool(GetGlobalConfig(), "BasicWindow", - "TransitionOnDoubleClick")) + if (!mouseSwitching) + return; + + if (!transitionOnDoubleClick) return; OBSBasic *main = (OBSBasic*)obs_frontend_get_main_window(); @@ -758,8 +821,8 @@ void OBSProjector::mouseDoubleClickEvent(QMouseEvent *event) return; if (event->button() == Qt::LeftButton) { - int pos = getSourceByPosition(event->x(), event->y()); - if (pos < 0) + int pos = getSourceByPosition(event->x(), event->y(), ratio); + if (pos < 0 || pos >= (int)numSrcs) return; OBSSource src = OBSGetStrongRef(multiviewScenes[pos]); if (!src) @@ -780,9 +843,12 @@ void OBSProjector::mousePressEvent(QMouseEvent *event) popup.exec(QCursor::pos()); } + if (!mouseSwitching) + return; + if (event->button() == Qt::LeftButton) { - int pos = getSourceByPosition(event->x(), event->y()); - if (pos < 0) + int pos = getSourceByPosition(event->x(), event->y(), ratio); + if (pos < 0 || pos >= (int)numSrcs) return; OBSSource src = OBSGetStrongRef(multiviewScenes[pos]); if (!src) @@ -796,36 +862,76 @@ void OBSProjector::mousePressEvent(QMouseEvent *event) void OBSProjector::EscapeTriggered() { - if (!isWindow) { - OBSBasic *main = (OBSBasic*)obs_frontend_get_main_window(); - main->RemoveSavedProjectors(savedMonitor); - } - deleteLater(); } void OBSProjector::UpdateMultiview() { - for (OBSWeakSource &val : multiviewScenes) - val = nullptr; - for (OBSSource &val : multiviewLabels) - val = nullptr; + multiviewScenes.clear(); + multiviewLabels.clear(); struct obs_video_info ovi; obs_get_video_info(&ovi); - uint32_t h = ovi.base_height; + uint32_t w = ovi.base_width; + uint32_t h = ovi.base_height; + fw = float(w); + fh = float(h); + ratio = fw / fh; struct obs_frontend_source_list scenes = {}; obs_frontend_get_scenes(&scenes); - int curIdx = 0; + multiviewLabels.emplace_back(CreateLabel(Str("StudioMode.Preview"), + h / 2)); + multiviewLabels.emplace_back(CreateLabel(Str("StudioMode.Program"), + h / 2)); - multiviewLabels[0] = CreateLabel(Str("StudioMode.Preview"), h / 2); - multiviewLabels[1] = CreateLabel(Str("StudioMode.Program"), h / 2); + multiviewLayout = static_cast(config_get_int( + GetGlobalConfig(), "BasicWindow", "MultiviewLayout")); - for (size_t i = 0; i < scenes.sources.num && curIdx < 8; i++) { - obs_source_t *src = scenes.sources.array[i]; + drawLabel = config_get_bool(GetGlobalConfig(), + "BasicWindow", "MultiviewDrawNames"); + + drawSafeArea = config_get_bool(GetGlobalConfig(), "BasicWindow", + "MultiviewDrawAreas"); + + mouseSwitching = config_get_bool(GetGlobalConfig(), "BasicWindow", + "MultiviewMouseSwitch"); + + transitionOnDoubleClick = config_get_bool(GetGlobalConfig(), + "BasicWindow", "TransitionOnDoubleClick"); + + switch(multiviewLayout) { + case MultiviewLayout::HORIZONTAL_TOP_24_SCENES: + pvwprgCX = fw / 3; + pvwprgCY = fh / 3; + + maxSrcs = 24; + break; + default: + pvwprgCX = fw / 2; + pvwprgCY = fh / 2; + + maxSrcs = 8; + } + + ppiCX = pvwprgCX - thicknessx2; + ppiCY = pvwprgCY - thicknessx2; + ppiScaleX = (pvwprgCX - thicknessx2) / fw; + ppiScaleY = (pvwprgCY - thicknessx2) / fh; + + scenesCX = pvwprgCX / 2; + scenesCY = pvwprgCY / 2; + siCX = scenesCX - thicknessx2; + siCY = scenesCY - thicknessx2; + siScaleX = (scenesCX - thicknessx2) / fw; + siScaleY = (scenesCY - thicknessx2) / fh; + + numSrcs = 0; + size_t i = 0; + while (i < scenes.sources.num && numSrcs < maxSrcs) { + obs_source_t *src = scenes.sources.array[i++]; OBSData data = obs_source_get_private_settings(src); obs_data_release(data); @@ -833,32 +939,53 @@ void OBSProjector::UpdateMultiview() if (!obs_data_get_bool(data, "show_in_multiview")) continue; - multiviewScenes[curIdx] = OBSGetWeakRef(src); + // We have a displayable source. + numSrcs++; + + multiviewScenes.emplace_back(OBSGetWeakRef(src)); obs_source_inc_showing(src); - std::string name; - name += std::to_string(curIdx + 1); - name += " - "; - name += obs_source_get_name(src); - - multiviewLabels[curIdx + 2] = CreateLabel(name.c_str(), h / 3); - - curIdx++; + std::string name = std::to_string(numSrcs) + " - " + + obs_source_get_name(src); + multiviewLabels.emplace_back(CreateLabel(name.c_str(), h / 3)); } obs_frontend_source_list_free(&scenes); +} - const char *multiviewLayoutText = config_get_string(GetGlobalConfig(), - "BasicWindow", "MultiviewLayout"); +void OBSProjector::UpdateProjectorTitle(QString name) +{ + projectorTitle = name; - if (astrcmpi(multiviewLayoutText, "horizontalbottom") == 0) - multiviewLayout = HORIZONTAL_BOTTOM; - else if (astrcmpi(multiviewLayoutText, "verticalleft") == 0) - multiviewLayout = VERTICAL_LEFT; - else if (astrcmpi(multiviewLayoutText, "verticalright") == 0) - multiviewLayout = VERTICAL_RIGHT; - else - multiviewLayout = HORIZONTAL_TOP; + QString title = nullptr; + switch (type) { + case ProjectorType::Scene: + title = QTStr("SceneWindow") + " - " + name; + break; + case ProjectorType::Source: + title = QTStr("SourceWindow") + " - " + name; + break; + default: + title = name; + break; + } + + setWindowTitle(title); +} + +OBSSource OBSProjector::GetSource() +{ + return source; +} + +ProjectorType OBSProjector::GetProjectorType() +{ + return type; +} + +int OBSProjector::GetMonitor() +{ + return savedMonitor; } void OBSProjector::UpdateMultiviewProjectors() @@ -874,3 +1001,10 @@ void OBSProjector::UpdateMultiviewProjectors() updatingMultiview = false; obs_leave_graphics(); } + +void OBSProjector::RenameProjector(QString oldName, QString newName) +{ + for (auto &projector : windowedProjectors) + if (projector->projectorTitle == oldName) + projector->UpdateProjectorTitle(newName); +} diff --git a/UI/window-projector.hpp b/UI/window-projector.hpp index 606ab61..1ec629d 100644 --- a/UI/window-projector.hpp +++ b/UI/window-projector.hpp @@ -2,10 +2,25 @@ #include #include "qt-display.hpp" -#include "window-basic-main.hpp" + +enum class ProjectorType { + Source, + Scene, + Preview, + StudioProgram, + Multiview +}; class QMouseEvent; +enum class MultiviewLayout : uint8_t { + HORIZONTAL_TOP_8_SCENES = 0, + HORIZONTAL_BOTTOM_8_SCENES = 1, + VERTICAL_LEFT_8_SCENES = 2, + VERTICAL_RIGHT_8_SCENES = 3, + HORIZONTAL_TOP_24_SCENES = 4 +}; + class OBSProjector : public OBSQTDisplay { Q_OBJECT @@ -20,31 +35,55 @@ private: void mousePressEvent(QMouseEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *event) override; - int savedMonitor = 0; - bool isWindow = false; + int savedMonitor; + bool isWindow; + QString projectorTitle; ProjectorType type = ProjectorType::Source; - OBSWeakSource multiviewScenes[8]; - OBSSource multiviewLabels[10]; - gs_vertbuffer_t *outerBox = nullptr; - gs_vertbuffer_t *innerBox = nullptr; - gs_vertbuffer_t *leftVLine = nullptr; - gs_vertbuffer_t *rightVLine = nullptr; - gs_vertbuffer_t *leftLine = nullptr; - gs_vertbuffer_t *topLine = nullptr; - gs_vertbuffer_t *rightLine = nullptr; + std::vector multiviewScenes; + std::vector multiviewLabels; + gs_vertbuffer_t *actionSafeMargin = nullptr; + gs_vertbuffer_t *graphicsSafeMargin = nullptr; + gs_vertbuffer_t *fourByThreeSafeMargin = nullptr; + gs_vertbuffer_t *leftLine = nullptr; + gs_vertbuffer_t *topLine = nullptr; + gs_vertbuffer_t *rightLine = nullptr; + gs_effect_t *solid = nullptr; + gs_eparam_t *color = nullptr; + // Multiview position helpers + float thickness = 4; + float offset, thicknessx2 = thickness * 2, pvwprgCX, + pvwprgCY, sourceX, sourceY, labelX, labelY, scenesCX, scenesCY, + ppiCX, ppiCY, siX, siY, siCX, siCY, ppiScaleX, ppiScaleY, + siScaleX, siScaleY, fw, fh, ratio; + + float lineLength = 0.1f; + // Rec. ITU-R BT.1848-1 / EBU R 95 + float actionSafePercentage = 0.035f; // 3.5% + float graphicsSafePercentage = 0.05f; // 5.0% + float fourByThreeSafePercentage = 0.1625f; // 16.25% bool ready = false; + // argb colors + static const uint32_t outerColor = 0xFFD0D0D0; + static const uint32_t labelColor = 0xD91F1F1F; + static const uint32_t backgroundColor = 0xFF000000; + static const uint32_t previewColor = 0xFF00D000; + static const uint32_t programColor = 0xFFD00000; + void UpdateMultiview(); + void UpdateProjectorTitle(QString name); private slots: void EscapeTriggered(); public: - OBSProjector(QWidget *parent, obs_source_t *source, bool window); + OBSProjector(QWidget *widget, obs_source_t *source_, int monitor, + QString title, ProjectorType type_); ~OBSProjector(); - void Init(int monitor, bool window, QString title, - ProjectorType type = ProjectorType::Source); - + OBSSource GetSource(); + ProjectorType GetProjectorType(); + int GetMonitor(); static void UpdateMultiviewProjectors(); + static void RenameProjector(QString oldName, QString newName); }; diff --git a/UI/window-remux.cpp b/UI/window-remux.cpp index d7308ca..92de804 100644 --- a/UI/window-remux.cpp +++ b/UI/window-remux.cpp @@ -200,11 +200,12 @@ void OBSRemux::updateProgress(float percent) void OBSRemux::remuxFinished(bool success) { + worker->job.reset(); + OBSMessageBox::information(this, QTStr("Remux.FinishedTitle"), success ? QTStr("Remux.Finished") : QTStr("Remux.FinishedError")); - worker->job.reset(); ui->progressBar->setVisible(false); ui->buttonBox->button(QDialogButtonBox::Ok)-> setEnabled(true); diff --git a/appveyor.yml b/appveyor.yml index 6d13f10..e679bfc 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,28 +1,31 @@ +image: Visual Studio 2017 environment: CURL_VERSION: 7.56.1 + CEF_VERSION: 3.3440.1805.gbe070f9 install: - git submodule update --init --recursive - - if exist dependencies2015.zip (curl -kLO https://obsproject.com/downloads/dependencies2015.zip -f --retry 5 -z dependencies2015.zip) else (curl -kLO https://obsproject.com/downloads/dependencies2015.zip -f --retry 5 -C -) + - if exist dependencies2017.zip (curl -kLO https://obsproject.com/downloads/dependencies2017.zip -f --retry 5 -z dependencies2017.zip) else (curl -kLO https://obsproject.com/downloads/dependencies2017.zip -f --retry 5 -C -) - if exist vlc.zip (curl -kLO https://obsproject.com/downloads/vlc.zip -f --retry 5 -z vlc.zip) else (curl -kLO https://obsproject.com/downloads/vlc.zip -f --retry 5 -C -) - - 7z x dependencies2015.zip -odependencies2015 + - if exist cef_binary_%CEF_VERSION%_windows32.zip (curl -kLO https://obsproject.com/downloads/cef_binary_%CEF_VERSION%_windows32.zip -f --retry 5 -z cef_binary_%CEF_VERSION%_windows32.zip) else (curl -kLO https://obsproject.com/downloads/cef_binary_%CEF_VERSION%_windows32.zip -f --retry 5 -C -) + - if exist cef_binary_%CEF_VERSION%_windows64.zip (curl -kLO https://obsproject.com/downloads/cef_binary_%CEF_VERSION%_windows64.zip -f --retry 5 -z cef_binary_%CEF_VERSION%_windows64.zip) else (curl -kLO https://obsproject.com/downloads/cef_binary_%CEF_VERSION%_windows64.zip -f --retry 5 -C -) + - 7z x dependencies2017.zip -odependencies2017 - 7z x vlc.zip -ovlc - - set DepsPath32=%CD%\dependencies2015\win32 - - set DepsPath64=%CD%\dependencies2015\win64 + - 7z x cef_binary_%CEF_VERSION%_windows32.zip -oCEF_32 + - 7z x cef_binary_%CEF_VERSION%_windows64.zip -oCEF_64 + - set DepsPath32=%CD%\dependencies2017\win32 + - set DepsPath64=%CD%\dependencies2017\win64 - set VLCPath=%CD%\vlc - - set QTDIR32=C:\Qt\5.7\msvc2015 - - set QTDIR64=C:\Qt\5.7\msvc2015_64 + - set QTDIR32=C:\Qt\5.11.1\msvc2015 + - set QTDIR64=C:\Qt\5.11.1\msvc2017_64 + - set CEF_32=%CD%\CEF_32\cef_binary_%CEF_VERSION%_windows32 + - set CEF_64=%CD%\CEF_64\cef_binary_%CEF_VERSION%_windows64 - set build_config=RelWithDebInfo - - move "C:\Program Files (x86)\Windows Kits\10\Include\10.0.14393.0\um\d3d11on12*" "C:\Program Files (x86)\Windows Kits\8.1\Include\um" - - move "C:\Program Files (x86)\Windows Kits\10\Include\10.0.14393.0\um\d3d12*" "C:\Program Files (x86)\Windows Kits\8.1\Include\um" - - move "C:\Program Files (x86)\Windows Kits\10\Include\10.0.14393.0\shared\dxgi*" "C:\Program Files (x86)\Windows Kits\8.1\Include\shared" - - mkdir build - - mkdir build32 - - mkdir build64 + - mkdir build build32 build64 - cd ./build32 - - cmake -G "Visual Studio 14 2015" -DCOPIED_DEPENDENCIES=false -DCOPY_DEPENDENCIES=true -DBUILD_CAPTIONS=true -DCOMPILE_D3D12_HOOK=true .. + - cmake -G "Visual Studio 15 2017" -DCOPIED_DEPENDENCIES=false -DCOPY_DEPENDENCIES=true -DBUILD_CAPTIONS=true -DCOMPILE_D3D12_HOOK=true -DBUILD_BROWSER=true -DCEF_ROOT_DIR=%CEF_32% .. - cd ../build64 - - cmake -G "Visual Studio 14 2015 Win64" -DCOPIED_DEPENDENCIES=false -DCOPY_DEPENDENCIES=true -DBUILD_CAPTIONS=true -DCOMPILE_D3D12_HOOK=true .. + - cmake -G "Visual Studio 15 2017 Win64" -DCOPIED_DEPENDENCIES=false -DCOPY_DEPENDENCIES=true -DBUILD_CAPTIONS=true -DCOMPILE_D3D12_HOOK=true -DBUILD_BROWSER=true -DCEF_ROOT_DIR=%CEF_64% .. build_script: - call msbuild /m /p:Configuration=%build_config% C:\projects\obs-studio\build32\obs-studio.sln /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" @@ -37,5 +40,15 @@ deploy_script: test: off cache: - - dependencies2015.zip + - dependencies2017.zip - vlc.zip + - 'cef_binary_%CEF_VERSION%_windows32.zip' + - 'cef_binary_%CEF_VERSION%_windows64.zip' + +notifications: + - provider: Webhook + url: + secure: k1kpaz4CB5Rg5a3MTb4XKnd76fJ+9ozz5RACVnNjdgmAjA1OSssZ6LZ3g0NGfzc/ + headers: + Authorization: + secure: A0PBwpHtsYzBOuye1EeS0fl562T0NZEInwZp0ZVER1wLQSeE6gzWGrRo2a0E7hii diff --git a/cmake/Modules/FindLuajit.cmake b/cmake/Modules/FindLuajit.cmake index 73e5e00..ddd0948 100644 --- a/cmake/Modules/FindLuajit.cmake +++ b/cmake/Modules/FindLuajit.cmake @@ -44,6 +44,10 @@ FIND_PATH(LUAJIT_INCLUDE_DIR include/luajit-2.0 luajit2.0 include/luajit2.0 + luajit-2.1 + include/luajit-2.1 + luajit2.1 + include/luajit2.1 ) find_library(LUAJIT_LIB diff --git a/cmake/Modules/FindMbedTLS.cmake b/cmake/Modules/FindMbedTLS.cmake new file mode 100644 index 0000000..954df18 --- /dev/null +++ b/cmake/Modules/FindMbedTLS.cmake @@ -0,0 +1,143 @@ +# Once done these will be defined: +# +# LIBMBEDTLS_FOUND +# LIBMBEDTLS_INCLUDE_DIRS +# LIBMBEDTLS_LIBRARIES +# +# For use in OBS: +# +# MBEDTLS_INCLUDE_DIR + +find_package(PkgConfig QUIET) +if (PKG_CONFIG_FOUND) + pkg_check_modules(_MBEDTLS QUIET mbedtls) +endif() + +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(_lib_suffix 64) +else() + set(_lib_suffix 32) +endif() + +# If we're on MacOS or Linux, please try to statically-link mbedtls. +if(STATIC_MBEDTLS AND (APPLE OR UNIX)) + set(_MBEDTLS_LIBRARIES libmbedtls.a) + set(_MBEDCRYPTO_LIBRARIES libmbedcrypto.a) + set(_MBEDX509_LIBRARIES libmbedx509.a) +endif() + +find_path(MBEDTLS_INCLUDE_DIR + NAMES mbedtls/ssl.h + HINTS + ENV mbedtlsPath${_lib_suffix} + ENV mbedtlsPath + ENV DepsPath${_lib_suffix} + ENV DepsPath + ${mbedtlsPath${_lib_suffix}} + ${mbedtlsPath} + ${DepsPath${_lib_suffix}} + ${DepsPath} + ${_MBEDTLS_INCLUDE_DIRS} + PATHS + /usr/include /usr/local/include /opt/local/include /sw/include + PATH_SUFFIXES + include) + +find_library(MBEDTLS_LIB + NAMES ${_MBEDTLS_LIBRARIES} mbedtls libmbedtls + HINTS + ENV mbedtlsPath${_lib_suffix} + ENV mbedtlsPath + ENV DepsPath${_lib_suffix} + ENV DepsPath + ${mbedtlsPath${_lib_suffix}} + ${mbedtlsPath} + ${DepsPath${_lib_suffix}} + ${DepsPath} + ${_MBEDTLS_LIBRARY_DIRS} + PATHS + /usr/lib /usr/local/lib /opt/local/lib /sw/lib + PATH_SUFFIXES + lib${_lib_suffix} lib + libs${_lib_suffix} libs + bin${_lib_suffix} bin + ../lib${_lib_suffix} ../lib + ../libs${_lib_suffix} ../libs + ../bin${_lib_suffix} ../bin) + +find_library(MBEDCRYPTO_LIB + NAMES ${_MBEDCRYPTO_LIBRARIES} mbedcrypto libmbedcrypto + HINTS + ENV mbedcryptoPath${_lib_suffix} + ENV mbedcryptoPath + ENV DepsPath${_lib_suffix} + ENV DepsPath + ${mbedcryptoPath${_lib_suffix}} + ${mbedcryptoPath} + ${DepsPath${_lib_suffix}} + ${DepsPath} + ${_MBEDCRYPTO_LIBRARY_DIRS} + PATHS + /usr/lib /usr/local/lib /opt/local/lib /sw/lib + PATH_SUFFIXES + lib${_lib_suffix} lib + libs${_lib_suffix} libs + bin${_lib_suffix} bin + ../lib${_lib_suffix} ../lib + ../libs${_lib_suffix} ../libs + ../bin${_lib_suffix} ../bin) + +find_library(MBEDX509_LIB + NAMES ${_MBEDX509_LIBRARIES} mbedx509 libmbedx509 + HINTS + ENV mbedx509Path${_lib_suffix} + ENV mbedx509Path + ENV DepsPath${_lib_suffix} + ENV DepsPath + ${mbedx509Path${_lib_suffix}} + ${mbedx509Path} + ${DepsPath${_lib_suffix}} + ${DepsPath} + ${_MBEDX509_LIBRARY_DIRS} + PATHS + /usr/lib /usr/local/lib /opt/local/lib /sw/lib + PATH_SUFFIXES + lib${_lib_suffix} lib + libs${_lib_suffix} libs + bin${_lib_suffix} bin + ../lib${_lib_suffix} ../lib + ../libs${_lib_suffix} ../libs + ../bin${_lib_suffix} ../bin) + +# Sometimes mbedtls is split between three libs, and sometimes it isn't. +# If it isn't, let's check if the symbols we need are all in MBEDTLS_LIB. +if(MBEDTLS_LIB AND NOT MBEDCRYPTO_LIB AND NOT MBEDX509_LIB) + set(CMAKE_REQUIRED_LIBRARIES ${MBEDTLS_LIB}) + set(CMAKE_REQUIRED_INCLUDES ${MBEDTLS_INCLUDE_DIR}) + check_symbol_exists(mbedtls_x509_crt_init "mbedtls/x509_crt.h" MBEDTLS_INCLUDES_X509) + check_symbol_exists(mbedtls_sha256_init "mbedtls/sha256.h" MBEDTLS_INCLUDES_CRYPTO) + unset(CMAKE_REQUIRED_INCLUDES) + unset(CMAKE_REQUIRED_LIBRARIES) +endif() + +# If we find all three libraries, then go ahead. +if(MBEDTLS_LIB AND MBEDCRYPTO_LIB AND MBEDX509_LIB) + set(LIBMBEDTLS_INCLUDE_DIRS ${MBEDTLS_INCLUDE_DIR}) + set(LIBMBEDTLS_LIBRARIES ${MBEDTLS_LIB} ${MBEDCRYPTO_LIB} ${MBEDX509_LIB}) + set(MBEDTLS_INCLUDE_DIRS ${LIBMBEDTLS_INCLUDE_DIRS}) + set(MBEDTLS_LIBRARIES ${LIBMBEDTLS_LIBRARIES}) + +# Otherwise, if we find MBEDTLS_LIB, and it has both CRYPTO and x509 +# within the single lib (i.e. a windows build environment), then also +# feel free to go ahead. +elseif(MBEDTLS_LIB AND MBEDTLS_INCLUDES_CRYPTO AND MBEDTLS_INCLUDES_X509) + set(LIBMBEDTLS_INCLUDE_DIRS ${MBEDTLS_INCLUDE_DIR}) + set(LIBMBEDTLS_LIBRARIES ${MBEDTLS_LIB}) + set(MBEDTLS_INCLUDE_DIRS ${LIBMBEDTLS_INCLUDE_DIRS}) + set(MBEDTLS_LIBRARIES ${LIBMBEDTLS_LIBRARIES}) +endif() + +# Now we've accounted for the 3-vs-1 library case: +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Libmbedtls DEFAULT_MSG MBEDTLS_LIBRARIES MBEDTLS_INCLUDE_DIRS) +mark_as_advanced(MBEDTLS_INCLUDE_DIR MBEDTLS_LIBRARIES MBEDTLS_INCLUDE_DIRS) diff --git a/cmake/Modules/FindSSL.cmake b/cmake/Modules/FindSSL.cmake deleted file mode 100644 index 5f5d50e..0000000 --- a/cmake/Modules/FindSSL.cmake +++ /dev/null @@ -1,77 +0,0 @@ -# Once done these will be defined: -# -# SSL_FOUND -# SSL_INCLUDE_DIRS -# SSL_LIBRARIES -# -# For use in OBS: -# -# SSL_INCLUDE_DIR - -find_package(PkgConfig QUIET) -if (PKG_CONFIG_FOUND) - pkg_check_modules(_CRYPTO QUIET libcrypto) - pkg_check_modules(_SSL QUIET libssl) -endif() - -if(CMAKE_SIZEOF_VOID_P EQUAL 8) - set(_lib_suffix 64) -else() - set(_lib_suffix 32) -endif() - -set(_SSL_BASE_HINTS - ENV sslPath${_lib_suffix} - ENV sslPath - ENV DepsPath${_lib_suffix} - ENV DepsPath - ${sslPath${_lib_suffix}} - ${sslPath} - ${DepsPath${_lib_suffix}} - ${DepsPath}) - -set(_SSL_LIB_SUFFIXES - lib${_lib_suffix} lib - libs${_lib_suffix} libs - bin${_lib_suffix} bin - ../lib${_lib_suffix} ../lib - ../libs${_lib_suffix} ../libs - ../bin${_lib_suffix} ../bin) - -find_path(SSL_INCLUDE_DIR - NAMES openssl/ssl.h - HINTS - ${_SSL_BASE_HINTS} - ${_CRYPTO_INCLUDE_DIRS} - ${_SSL_INCLUDE_DIRS} - PATHS - /usr/include /usr/local/include /opt/local/include /sw/include - PATH_SUFFIXES - include) - -find_library(_SSL_LIB - NAMES ${_SSL_LIBRARIES} ssleay32 ssl - HINTS - ${_SSL_BASE_HINTS} - ${_SSL_LIBRARY_DIRS} - PATHS - /usr/lib /usr/local/lib /opt/local/lib /sw/lib - PATH_SUFFIXES ${_SSL_LIB_SUFFIXES}) - -find_library(_CRYPTO_LIB - NAMES ${_CRYPTO_LIBRARIES} libeay32 crypto - HINTS - ${_SSL_BASE_HINTS} - ${_CRYPTO_LIBRARY_DIRS} - PATHS - /usr/lib /usr/local/lib /opt/local/lib /sw/lib - PATH_SUFFIXES ${_SSL_LIB_SUFFIXES}) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(ssl DEFAULT_MSG _SSL_LIB _CRYPTO_LIB SSL_INCLUDE_DIR) -mark_as_advanced(SSL_INCLUDE_DIR _SSL_LIB _CRYPTO_LIB) - -if(SSL_FOUND) - set(SSL_INCLUDE_DIRS ${SSL_INCLUDE_DIR}) - set(SSL_LIBRARIES ${_SSL_LIB} ${_CRYPTO_LIB}) -endif() diff --git a/cmake/Modules/FindXCB.cmake b/cmake/Modules/FindXCB.cmake index efeea58..7f948bc 100644 --- a/cmake/Modules/FindXCB.cmake +++ b/cmake/Modules/FindXCB.cmake @@ -21,6 +21,7 @@ # XCB_GLX_FOUND XCB_GLX_INCLUDE_DIR XCB_GLX_LIBRARY # XCB_SHM_FOUND XCB_SHM_INCLUDE_DIR XCB_SHM_LIBRARY # XCB_XV_FOUND XCB_XV_INCLUDE_DIR XCB_XV_LIBRARY +# XCB_XINPUT_FOUND XCB_XINPUT_INCLUDE_DIR XCB_XINPUT_LIBRARY # XCB_SYNC_FOUND XCB_SYNC_INCLUDE_DIR XCB_SYNC_LIBRARY # XCB_XTEST_FOUND XCB_XTEST_INCLUDE_DIR XCB_XTEST_LIBRARY # XCB_ICCCM_FOUND XCB_ICCCM_INCLUDE_DIR XCB_ICCCM_LIBRARY @@ -54,6 +55,7 @@ set(knownComponents XCB XFIXES XTEST XV + XINPUT XINERAMA) unset(unknownComponents) @@ -112,6 +114,8 @@ foreach(comp ${comps}) list(APPEND pkgConfigModules "xcb-xtest") elseif("${comp}" STREQUAL "XV") list(APPEND pkgConfigModules "xcb-xv") + elseif("${comp}" STREQUAL "XINPUT") + list(APPEND pkgConfigModules "xcb-xinput") elseif("${comp}" STREQUAL "XINERAMA") list(APPEND pkgConfigModules "xcb-xinerama") endif() @@ -190,6 +194,9 @@ macro(_XCB_HANDLE_COMPONENT _comp) elseif("${_comp}" STREQUAL "XV") set(_header "xcb/xv.h") set(_lib "xcb-xv") + elseif("${_comp}" STREQUAL "XINPUT") + set(_header "xcb/xinput.h") + set(_lib "xcb-xinput") elseif("${_comp}" STREQUAL "XINERAMA") set(_header "xcb/xinerama.h") set(_lib "xcb-xinerama") diff --git a/cmake/osxbundle/Info.plist b/cmake/osxbundle/Info.plist index 443e456..15a7326 100644 --- a/cmake/osxbundle/Info.plist +++ b/cmake/osxbundle/Info.plist @@ -22,5 +22,9 @@ LSAppNapIsDisabled + NSCameraUsageDescription + OBS needs to access the camera to enable camera sources to work. + NSMicrophoneUsageDescription + OBS needs to access the microphone to enable audio input. diff --git a/deps/json11/LICENSE.txt b/deps/json11/LICENSE.txt new file mode 100644 index 0000000..691742e --- /dev/null +++ b/deps/json11/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2013 Dropbox, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/deps/json11/json11.cpp b/deps/json11/json11.cpp new file mode 100644 index 0000000..9647846 --- /dev/null +++ b/deps/json11/json11.cpp @@ -0,0 +1,788 @@ +/* Copyright (c) 2013 Dropbox, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "json11.hpp" +#include +#include +#include +#include +#include + +namespace json11 { + +static const int max_depth = 200; + +using std::string; +using std::vector; +using std::map; +using std::make_shared; +using std::initializer_list; +using std::move; + +/* Helper for representing null - just a do-nothing struct, plus comparison + * operators so the helpers in JsonValue work. We can't use nullptr_t because + * it may not be orderable. + */ +struct NullStruct { + bool operator==(NullStruct) const { return true; } + bool operator<(NullStruct) const { return false; } +}; + +/* * * * * * * * * * * * * * * * * * * * + * Serialization + */ + +static void dump(NullStruct, string &out) { + out += "null"; +} + +static void dump(double value, string &out) { + if (std::isfinite(value)) { + char buf[32]; + snprintf(buf, sizeof buf, "%.17g", value); + out += buf; + } else { + out += "null"; + } +} + +static void dump(int value, string &out) { + char buf[32]; + snprintf(buf, sizeof buf, "%d", value); + out += buf; +} + +static void dump(bool value, string &out) { + out += value ? "true" : "false"; +} + +static void dump(const string &value, string &out) { + out += '"'; + for (size_t i = 0; i < value.length(); i++) { + const char ch = value[i]; + if (ch == '\\') { + out += "\\\\"; + } else if (ch == '"') { + out += "\\\""; + } else if (ch == '\b') { + out += "\\b"; + } else if (ch == '\f') { + out += "\\f"; + } else if (ch == '\n') { + out += "\\n"; + } else if (ch == '\r') { + out += "\\r"; + } else if (ch == '\t') { + out += "\\t"; + } else if (static_cast(ch) <= 0x1f) { + char buf[8]; + snprintf(buf, sizeof buf, "\\u%04x", ch); + out += buf; + } else if (static_cast(ch) == 0xe2 && static_cast(value[i+1]) == 0x80 + && static_cast(value[i+2]) == 0xa8) { + out += "\\u2028"; + i += 2; + } else if (static_cast(ch) == 0xe2 && static_cast(value[i+1]) == 0x80 + && static_cast(value[i+2]) == 0xa9) { + out += "\\u2029"; + i += 2; + } else { + out += ch; + } + } + out += '"'; +} + +static void dump(const Json::array &values, string &out) { + bool first = true; + out += "["; + for (const auto &value : values) { + if (!first) + out += ", "; + value.dump(out); + first = false; + } + out += "]"; +} + +static void dump(const Json::object &values, string &out) { + bool first = true; + out += "{"; + for (const auto &kv : values) { + if (!first) + out += ", "; + dump(kv.first, out); + out += ": "; + kv.second.dump(out); + first = false; + } + out += "}"; +} + +void Json::dump(string &out) const { + m_ptr->dump(out); +} + +/* * * * * * * * * * * * * * * * * * * * + * Value wrappers + */ + +template +class Value : public JsonValue { +protected: + + // Constructors + explicit Value(const T &value) : m_value(value) {} + explicit Value(T &&value) : m_value(move(value)) {} + + // Get type tag + Json::Type type() const override { + return tag; + } + + // Comparisons + bool equals(const JsonValue * other) const override { + return m_value == static_cast *>(other)->m_value; + } + bool less(const JsonValue * other) const override { + return m_value < static_cast *>(other)->m_value; + } + + const T m_value; + void dump(string &out) const override { json11::dump(m_value, out); } +}; + +class JsonDouble final : public Value { + double number_value() const override { return m_value; } + int int_value() const override { return static_cast(m_value); } + bool equals(const JsonValue * other) const override { return m_value == other->number_value(); } + bool less(const JsonValue * other) const override { return m_value < other->number_value(); } +public: + explicit JsonDouble(double value) : Value(value) {} +}; + +class JsonInt final : public Value { + double number_value() const override { return m_value; } + int int_value() const override { return m_value; } + bool equals(const JsonValue * other) const override { return m_value == other->number_value(); } + bool less(const JsonValue * other) const override { return m_value < other->number_value(); } +public: + explicit JsonInt(int value) : Value(value) {} +}; + +class JsonBoolean final : public Value { + bool bool_value() const override { return m_value; } +public: + explicit JsonBoolean(bool value) : Value(value) {} +}; + +class JsonString final : public Value { + const string &string_value() const override { return m_value; } +public: + explicit JsonString(const string &value) : Value(value) {} + explicit JsonString(string &&value) : Value(move(value)) {} +}; + +class JsonArray final : public Value { + const Json::array &array_items() const override { return m_value; } + const Json & operator[](size_t i) const override; +public: + explicit JsonArray(const Json::array &value) : Value(value) {} + explicit JsonArray(Json::array &&value) : Value(move(value)) {} +}; + +class JsonObject final : public Value { + const Json::object &object_items() const override { return m_value; } + const Json & operator[](const string &key) const override; +public: + explicit JsonObject(const Json::object &value) : Value(value) {} + explicit JsonObject(Json::object &&value) : Value(move(value)) {} +}; + +class JsonNull final : public Value { +public: + JsonNull() : Value({}) {} +}; + +/* * * * * * * * * * * * * * * * * * * * + * Static globals - static-init-safe + */ +struct Statics { + const std::shared_ptr null = make_shared(); + const std::shared_ptr t = make_shared(true); + const std::shared_ptr f = make_shared(false); + const string empty_string; + const vector empty_vector; + const map empty_map; + Statics() {} +}; + +static const Statics & statics() { + static const Statics s {}; + return s; +} + +static const Json & static_null() { + // This has to be separate, not in Statics, because Json() accesses statics().null. + static const Json json_null; + return json_null; +} + +/* * * * * * * * * * * * * * * * * * * * + * Constructors + */ + +Json::Json() noexcept : m_ptr(statics().null) {} +Json::Json(std::nullptr_t) noexcept : m_ptr(statics().null) {} +Json::Json(double value) : m_ptr(make_shared(value)) {} +Json::Json(int value) : m_ptr(make_shared(value)) {} +Json::Json(bool value) : m_ptr(value ? statics().t : statics().f) {} +Json::Json(const string &value) : m_ptr(make_shared(value)) {} +Json::Json(string &&value) : m_ptr(make_shared(move(value))) {} +Json::Json(const char * value) : m_ptr(make_shared(value)) {} +Json::Json(const Json::array &values) : m_ptr(make_shared(values)) {} +Json::Json(Json::array &&values) : m_ptr(make_shared(move(values))) {} +Json::Json(const Json::object &values) : m_ptr(make_shared(values)) {} +Json::Json(Json::object &&values) : m_ptr(make_shared(move(values))) {} + +/* * * * * * * * * * * * * * * * * * * * + * Accessors + */ + +Json::Type Json::type() const { return m_ptr->type(); } +double Json::number_value() const { return m_ptr->number_value(); } +int Json::int_value() const { return m_ptr->int_value(); } +bool Json::bool_value() const { return m_ptr->bool_value(); } +const string & Json::string_value() const { return m_ptr->string_value(); } +const vector & Json::array_items() const { return m_ptr->array_items(); } +const map & Json::object_items() const { return m_ptr->object_items(); } +const Json & Json::operator[] (size_t i) const { return (*m_ptr)[i]; } +const Json & Json::operator[] (const string &key) const { return (*m_ptr)[key]; } + +double JsonValue::number_value() const { return 0; } +int JsonValue::int_value() const { return 0; } +bool JsonValue::bool_value() const { return false; } +const string & JsonValue::string_value() const { return statics().empty_string; } +const vector & JsonValue::array_items() const { return statics().empty_vector; } +const map & JsonValue::object_items() const { return statics().empty_map; } +const Json & JsonValue::operator[] (size_t) const { return static_null(); } +const Json & JsonValue::operator[] (const string &) const { return static_null(); } + +const Json & JsonObject::operator[] (const string &key) const { + auto iter = m_value.find(key); + return (iter == m_value.end()) ? static_null() : iter->second; +} +const Json & JsonArray::operator[] (size_t i) const { + if (i >= m_value.size()) return static_null(); + else return m_value[i]; +} + +/* * * * * * * * * * * * * * * * * * * * + * Comparison + */ + +bool Json::operator== (const Json &other) const { + if (m_ptr == other.m_ptr) + return true; + if (m_ptr->type() != other.m_ptr->type()) + return false; + + return m_ptr->equals(other.m_ptr.get()); +} + +bool Json::operator< (const Json &other) const { + if (m_ptr == other.m_ptr) + return false; + if (m_ptr->type() != other.m_ptr->type()) + return m_ptr->type() < other.m_ptr->type(); + + return m_ptr->less(other.m_ptr.get()); +} + +/* * * * * * * * * * * * * * * * * * * * + * Parsing + */ + +/* esc(c) + * + * Format char c suitable for printing in an error message. + */ +static inline string esc(char c) { + char buf[12]; + if (static_cast(c) >= 0x20 && static_cast(c) <= 0x7f) { + snprintf(buf, sizeof buf, "'%c' (%d)", c, c); + } else { + snprintf(buf, sizeof buf, "(%d)", c); + } + return string(buf); +} + +static inline bool in_range(long x, long lower, long upper) { + return (x >= lower && x <= upper); +} + +namespace { +/* JsonParser + * + * Object that tracks all state of an in-progress parse. + */ +struct JsonParser final { + + /* State + */ + const string &str; + size_t i; + string &err; + bool failed; + const JsonParse strategy; + + /* fail(msg, err_ret = Json()) + * + * Mark this parse as failed. + */ + Json fail(string &&msg) { + return fail(move(msg), Json()); + } + + template + T fail(string &&msg, const T err_ret) { + if (!failed) + err = std::move(msg); + failed = true; + return err_ret; + } + + /* consume_whitespace() + * + * Advance until the current character is non-whitespace. + */ + void consume_whitespace() { + while (str[i] == ' ' || str[i] == '\r' || str[i] == '\n' || str[i] == '\t') + i++; + } + + /* consume_comment() + * + * Advance comments (c-style inline and multiline). + */ + bool consume_comment() { + bool comment_found = false; + if (str[i] == '/') { + i++; + if (i == str.size()) + return fail("unexpected end of input after start of comment", false); + if (str[i] == '/') { // inline comment + i++; + // advance until next line, or end of input + while (i < str.size() && str[i] != '\n') { + i++; + } + comment_found = true; + } + else if (str[i] == '*') { // multiline comment + i++; + if (i > str.size()-2) + return fail("unexpected end of input inside multi-line comment", false); + // advance until closing tokens + while (!(str[i] == '*' && str[i+1] == '/')) { + i++; + if (i > str.size()-2) + return fail( + "unexpected end of input inside multi-line comment", false); + } + i += 2; + comment_found = true; + } + else + return fail("malformed comment", false); + } + return comment_found; + } + + /* consume_garbage() + * + * Advance until the current character is non-whitespace and non-comment. + */ + void consume_garbage() { + consume_whitespace(); + if(strategy == JsonParse::COMMENTS) { + bool comment_found = false; + do { + comment_found = consume_comment(); + if (failed) return; + consume_whitespace(); + } + while(comment_found); + } + } + + /* get_next_token() + * + * Return the next non-whitespace character. If the end of the input is reached, + * flag an error and return 0. + */ + char get_next_token() { + consume_garbage(); + if (failed) return (char)0; + if (i == str.size()) + return fail("unexpected end of input", (char)0); + + return str[i++]; + } + + /* encode_utf8(pt, out) + * + * Encode pt as UTF-8 and add it to out. + */ + void encode_utf8(long pt, string & out) { + if (pt < 0) + return; + + if (pt < 0x80) { + out += static_cast(pt); + } else if (pt < 0x800) { + out += static_cast((pt >> 6) | 0xC0); + out += static_cast((pt & 0x3F) | 0x80); + } else if (pt < 0x10000) { + out += static_cast((pt >> 12) | 0xE0); + out += static_cast(((pt >> 6) & 0x3F) | 0x80); + out += static_cast((pt & 0x3F) | 0x80); + } else { + out += static_cast((pt >> 18) | 0xF0); + out += static_cast(((pt >> 12) & 0x3F) | 0x80); + out += static_cast(((pt >> 6) & 0x3F) | 0x80); + out += static_cast((pt & 0x3F) | 0x80); + } + } + + /* parse_string() + * + * Parse a string, starting at the current position. + */ + string parse_string() { + string out; + long last_escaped_codepoint = -1; + while (true) { + if (i == str.size()) + return fail("unexpected end of input in string", ""); + + char ch = str[i++]; + + if (ch == '"') { + encode_utf8(last_escaped_codepoint, out); + return out; + } + + if (in_range(ch, 0, 0x1f)) + return fail("unescaped " + esc(ch) + " in string", ""); + + // The usual case: non-escaped characters + if (ch != '\\') { + encode_utf8(last_escaped_codepoint, out); + last_escaped_codepoint = -1; + out += ch; + continue; + } + + // Handle escapes + if (i == str.size()) + return fail("unexpected end of input in string", ""); + + ch = str[i++]; + + if (ch == 'u') { + // Extract 4-byte escape sequence + string esc = str.substr(i, 4); + // Explicitly check length of the substring. The following loop + // relies on std::string returning the terminating NUL when + // accessing str[length]. Checking here reduces brittleness. + if (esc.length() < 4) { + return fail("bad \\u escape: " + esc, ""); + } + for (size_t j = 0; j < 4; j++) { + if (!in_range(esc[j], 'a', 'f') && !in_range(esc[j], 'A', 'F') + && !in_range(esc[j], '0', '9')) + return fail("bad \\u escape: " + esc, ""); + } + + long codepoint = strtol(esc.data(), nullptr, 16); + + // JSON specifies that characters outside the BMP shall be encoded as a pair + // of 4-hex-digit \u escapes encoding their surrogate pair components. Check + // whether we're in the middle of such a beast: the previous codepoint was an + // escaped lead (high) surrogate, and this is a trail (low) surrogate. + if (in_range(last_escaped_codepoint, 0xD800, 0xDBFF) + && in_range(codepoint, 0xDC00, 0xDFFF)) { + // Reassemble the two surrogate pairs into one astral-plane character, per + // the UTF-16 algorithm. + encode_utf8((((last_escaped_codepoint - 0xD800) << 10) + | (codepoint - 0xDC00)) + 0x10000, out); + last_escaped_codepoint = -1; + } else { + encode_utf8(last_escaped_codepoint, out); + last_escaped_codepoint = codepoint; + } + + i += 4; + continue; + } + + encode_utf8(last_escaped_codepoint, out); + last_escaped_codepoint = -1; + + if (ch == 'b') { + out += '\b'; + } else if (ch == 'f') { + out += '\f'; + } else if (ch == 'n') { + out += '\n'; + } else if (ch == 'r') { + out += '\r'; + } else if (ch == 't') { + out += '\t'; + } else if (ch == '"' || ch == '\\' || ch == '/') { + out += ch; + } else { + return fail("invalid escape character " + esc(ch), ""); + } + } + } + + /* parse_number() + * + * Parse a double. + */ + Json parse_number() { + size_t start_pos = i; + + if (str[i] == '-') + i++; + + // Integer part + if (str[i] == '0') { + i++; + if (in_range(str[i], '0', '9')) + return fail("leading 0s not permitted in numbers"); + } else if (in_range(str[i], '1', '9')) { + i++; + while (in_range(str[i], '0', '9')) + i++; + } else { + return fail("invalid " + esc(str[i]) + " in number"); + } + + if (str[i] != '.' && str[i] != 'e' && str[i] != 'E' + && (i - start_pos) <= static_cast(std::numeric_limits::digits10)) { + return std::atoi(str.c_str() + start_pos); + } + + // Decimal part + if (str[i] == '.') { + i++; + if (!in_range(str[i], '0', '9')) + return fail("at least one digit required in fractional part"); + + while (in_range(str[i], '0', '9')) + i++; + } + + // Exponent part + if (str[i] == 'e' || str[i] == 'E') { + i++; + + if (str[i] == '+' || str[i] == '-') + i++; + + if (!in_range(str[i], '0', '9')) + return fail("at least one digit required in exponent"); + + while (in_range(str[i], '0', '9')) + i++; + } + + return std::strtod(str.c_str() + start_pos, nullptr); + } + + /* expect(str, res) + * + * Expect that 'str' starts at the character that was just read. If it does, advance + * the input and return res. If not, flag an error. + */ + Json expect(const string &expected, Json res) { + assert(i != 0); + i--; + if (str.compare(i, expected.length(), expected) == 0) { + i += expected.length(); + return res; + } else { + return fail("parse error: expected " + expected + ", got " + str.substr(i, expected.length())); + } + } + + /* parse_json() + * + * Parse a JSON object. + */ + Json parse_json(int depth) { + if (depth > max_depth) { + return fail("exceeded maximum nesting depth"); + } + + char ch = get_next_token(); + if (failed) + return Json(); + + if (ch == '-' || (ch >= '0' && ch <= '9')) { + i--; + return parse_number(); + } + + if (ch == 't') + return expect("true", true); + + if (ch == 'f') + return expect("false", false); + + if (ch == 'n') + return expect("null", Json()); + + if (ch == '"') + return parse_string(); + + if (ch == '{') { + map data; + ch = get_next_token(); + if (ch == '}') + return data; + + while (1) { + if (ch != '"') + return fail("expected '\"' in object, got " + esc(ch)); + + string key = parse_string(); + if (failed) + return Json(); + + ch = get_next_token(); + if (ch != ':') + return fail("expected ':' in object, got " + esc(ch)); + + data[std::move(key)] = parse_json(depth + 1); + if (failed) + return Json(); + + ch = get_next_token(); + if (ch == '}') + break; + if (ch != ',') + return fail("expected ',' in object, got " + esc(ch)); + + ch = get_next_token(); + } + return data; + } + + if (ch == '[') { + vector data; + ch = get_next_token(); + if (ch == ']') + return data; + + while (1) { + i--; + data.push_back(parse_json(depth + 1)); + if (failed) + return Json(); + + ch = get_next_token(); + if (ch == ']') + break; + if (ch != ',') + return fail("expected ',' in list, got " + esc(ch)); + + ch = get_next_token(); + (void)ch; + } + return data; + } + + return fail("expected value, got " + esc(ch)); + } +}; +}//namespace { + +Json Json::parse(const string &in, string &err, JsonParse strategy) { + JsonParser parser { in, 0, err, false, strategy }; + Json result = parser.parse_json(0); + + // Check for any trailing garbage + parser.consume_garbage(); + if (parser.failed) + return Json(); + if (parser.i != in.size()) + return parser.fail("unexpected trailing " + esc(in[parser.i])); + + return result; +} + +// Documented in json11.hpp +vector Json::parse_multi(const string &in, + std::string::size_type &parser_stop_pos, + string &err, + JsonParse strategy) { + JsonParser parser { in, 0, err, false, strategy }; + parser_stop_pos = 0; + vector json_vec; + while (parser.i != in.size() && !parser.failed) { + json_vec.push_back(parser.parse_json(0)); + if (parser.failed) + break; + + // Check for another object + parser.consume_garbage(); + if (parser.failed) + break; + parser_stop_pos = parser.i; + } + return json_vec; +} + +/* * * * * * * * * * * * * * * * * * * * + * Shape-checking + */ + +bool Json::has_shape(const shape & types, string & err) const { + if (!is_object()) { + err = "expected JSON object, got " + dump(); + return false; + } + + for (auto & item : types) { + if ((*this)[item.first].type() != item.second) { + err = "bad type for " + item.first + " in " + dump(); + return false; + } + } + + return true; +} + +} // namespace json11 diff --git a/deps/json11/json11.hpp b/deps/json11/json11.hpp new file mode 100644 index 0000000..0c47d05 --- /dev/null +++ b/deps/json11/json11.hpp @@ -0,0 +1,232 @@ +/* json11 + * + * json11 is a tiny JSON library for C++11, providing JSON parsing and serialization. + * + * The core object provided by the library is json11::Json. A Json object represents any JSON + * value: null, bool, number (int or double), string (std::string), array (std::vector), or + * object (std::map). + * + * Json objects act like values: they can be assigned, copied, moved, compared for equality or + * order, etc. There are also helper methods Json::dump, to serialize a Json to a string, and + * Json::parse (static) to parse a std::string as a Json object. + * + * Internally, the various types of Json object are represented by the JsonValue class + * hierarchy. + * + * A note on numbers - JSON specifies the syntax of number formatting but not its semantics, + * so some JSON implementations distinguish between integers and floating-point numbers, while + * some don't. In json11, we choose the latter. Because some JSON implementations (namely + * Javascript itself) treat all numbers as the same type, distinguishing the two leads + * to JSON that will be *silently* changed by a round-trip through those implementations. + * Dangerous! To avoid that risk, json11 stores all numbers as double internally, but also + * provides integer helpers. + * + * Fortunately, double-precision IEEE754 ('double') can precisely store any integer in the + * range +/-2^53, which includes every 'int' on most systems. (Timestamps often use int64 + * or long long to avoid the Y2038K problem; a double storing microseconds since some epoch + * will be exact for +/- 275 years.) + */ + +/* Copyright (c) 2013 Dropbox, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#ifdef _MSC_VER + #if _MSC_VER <= 1800 // VS 2013 + #ifndef noexcept + #define noexcept throw() + #endif + + #ifndef snprintf + #define snprintf _snprintf_s + #endif + #endif +#endif + +namespace json11 { + +enum JsonParse { + STANDARD, COMMENTS +}; + +class JsonValue; + +class Json final { +public: + // Types + enum Type { + NUL, NUMBER, BOOL, STRING, ARRAY, OBJECT + }; + + // Array and object typedefs + typedef std::vector array; + typedef std::map object; + + // Constructors for the various types of JSON value. + Json() noexcept; // NUL + Json(std::nullptr_t) noexcept; // NUL + Json(double value); // NUMBER + Json(int value); // NUMBER + Json(bool value); // BOOL + Json(const std::string &value); // STRING + Json(std::string &&value); // STRING + Json(const char * value); // STRING + Json(const array &values); // ARRAY + Json(array &&values); // ARRAY + Json(const object &values); // OBJECT + Json(object &&values); // OBJECT + + // Implicit constructor: anything with a to_json() function. + template + Json(const T & t) : Json(t.to_json()) {} + + // Implicit constructor: map-like objects (std::map, std::unordered_map, etc) + template ().begin()->first)>::value + && std::is_constructible().begin()->second)>::value, + int>::type = 0> + Json(const M & m) : Json(object(m.begin(), m.end())) {} + + // Implicit constructor: vector-like objects (std::list, std::vector, std::set, etc) + template ().begin())>::value, + int>::type = 0> + Json(const V & v) : Json(array(v.begin(), v.end())) {} + + // This prevents Json(some_pointer) from accidentally producing a bool. Use + // Json(bool(some_pointer)) if that behavior is desired. + Json(void *) = delete; + + // Accessors + Type type() const; + + bool is_null() const { return type() == NUL; } + bool is_number() const { return type() == NUMBER; } + bool is_bool() const { return type() == BOOL; } + bool is_string() const { return type() == STRING; } + bool is_array() const { return type() == ARRAY; } + bool is_object() const { return type() == OBJECT; } + + // Return the enclosed value if this is a number, 0 otherwise. Note that json11 does not + // distinguish between integer and non-integer numbers - number_value() and int_value() + // can both be applied to a NUMBER-typed object. + double number_value() const; + int int_value() const; + + // Return the enclosed value if this is a boolean, false otherwise. + bool bool_value() const; + // Return the enclosed string if this is a string, "" otherwise. + const std::string &string_value() const; + // Return the enclosed std::vector if this is an array, or an empty vector otherwise. + const array &array_items() const; + // Return the enclosed std::map if this is an object, or an empty map otherwise. + const object &object_items() const; + + // Return a reference to arr[i] if this is an array, Json() otherwise. + const Json & operator[](size_t i) const; + // Return a reference to obj[key] if this is an object, Json() otherwise. + const Json & operator[](const std::string &key) const; + + // Serialize. + void dump(std::string &out) const; + std::string dump() const { + std::string out; + dump(out); + return out; + } + + // Parse. If parse fails, return Json() and assign an error message to err. + static Json parse(const std::string & in, + std::string & err, + JsonParse strategy = JsonParse::STANDARD); + static Json parse(const char * in, + std::string & err, + JsonParse strategy = JsonParse::STANDARD) { + if (in) { + return parse(std::string(in), err, strategy); + } else { + err = "null input"; + return nullptr; + } + } + // Parse multiple objects, concatenated or separated by whitespace + static std::vector parse_multi( + const std::string & in, + std::string::size_type & parser_stop_pos, + std::string & err, + JsonParse strategy = JsonParse::STANDARD); + + static inline std::vector parse_multi( + const std::string & in, + std::string & err, + JsonParse strategy = JsonParse::STANDARD) { + std::string::size_type parser_stop_pos; + return parse_multi(in, parser_stop_pos, err, strategy); + } + + bool operator== (const Json &rhs) const; + bool operator< (const Json &rhs) const; + bool operator!= (const Json &rhs) const { return !(*this == rhs); } + bool operator<= (const Json &rhs) const { return !(rhs < *this); } + bool operator> (const Json &rhs) const { return (rhs < *this); } + bool operator>= (const Json &rhs) const { return !(*this < rhs); } + + /* has_shape(types, err) + * + * Return true if this is a JSON object and, for each item in types, has a field of + * the given type. If not, return false and set err to a descriptive message. + */ + typedef std::initializer_list> shape; + bool has_shape(const shape & types, std::string & err) const; + +private: + std::shared_ptr m_ptr; +}; + +// Internal class hierarchy - JsonValue objects are not exposed to users of this API. +class JsonValue { +protected: + friend class Json; + friend class JsonInt; + friend class JsonDouble; + virtual Json::Type type() const = 0; + virtual bool equals(const JsonValue * other) const = 0; + virtual bool less(const JsonValue * other) const = 0; + virtual void dump(std::string &out) const = 0; + virtual double number_value() const; + virtual int int_value() const; + virtual bool bool_value() const; + virtual const std::string &string_value() const; + virtual const Json::array &array_items() const; + virtual const Json &operator[](size_t i) const; + virtual const Json::object &object_items() const; + virtual const Json &operator[](const std::string &key) const; + virtual ~JsonValue() {} +}; + +} // namespace json11 diff --git a/docs/sphinx/reference-core.rst b/docs/sphinx/reference-core.rst index d5655b8..bc44083 100644 --- a/docs/sphinx/reference-core.rst +++ b/docs/sphinx/reference-core.rst @@ -433,6 +433,18 @@ Video, Audio, and Graphics Adds/removes a main rendering callback. Allows custom rendering to the main stream/recording output. +--------------------- + +.. function:: void obs_add_raw_video_callback(const struct video_scale_info *conversion, void (*callback)(void *param, struct video_data *frame), void *param) + void obs_remove_raw_video_callback(void (*callback)(void *param, struct video_data *frame), void *param) + + Adds/removes a raw video callback. Allows the ability to obtain raw + video frames without necessarily using an output. + + :param conversion: Specifies conversion requirements. Can be NULL. + :param callback: The callback that receives raw video frames. + :param param: The private data associated with the callback. + Primary signal/procedure handlers --------------------------------- diff --git a/docs/sphinx/reference-encoders.rst b/docs/sphinx/reference-encoders.rst index 37cde83..a9b4728 100644 --- a/docs/sphinx/reference-encoders.rst +++ b/docs/sphinx/reference-encoders.rst @@ -407,6 +407,7 @@ General Encoder Functions --------------------- .. function:: obs_data_t *obs_encoder_defaults(const char *id) + obs_data_t *obs_encoder_get_defaults(const obs_encoder_t *encoder) :return: An incremented reference to the encoder's default settings diff --git a/docs/sphinx/reference-libobs-callback.rst b/docs/sphinx/reference-libobs-callback.rst index 6a954dd..40e3271 100644 --- a/docs/sphinx/reference-libobs-callback.rst +++ b/docs/sphinx/reference-libobs-callback.rst @@ -196,6 +196,18 @@ Signals are used for all event-based callbacks. --------------------- +.. function:: void signal_handler_connect_ref(signal_handler_t *handler, const char *signal, signal_callback_t callback, void *data) + + Connect a callback to a signal on a signal handler, and increments + the handler's internal reference counter, preventing it from being + destroyed until the signal has been disconnected. + + :param handler: Signal handler object + :param callback: Signal callback + :param data: Private data passed the callback + +--------------------- + .. function:: void signal_handler_disconnect(signal_handler_t *handler, const char *signal, signal_callback_t callback, void *data) Disconnects a callback from a signal on a signal handler. diff --git a/docs/sphinx/reference-outputs.rst b/docs/sphinx/reference-outputs.rst index 6d671fb..404dd23 100644 --- a/docs/sphinx/reference-outputs.rst +++ b/docs/sphinx/reference-outputs.rst @@ -586,6 +586,13 @@ General Output Functions --------------------- +.. function:: uint32_t obs_output_get_flags(const obs_output_t *output) + uint32_t obs_get_output_flags(const char *id) + + :return: The output capability flags + +--------------------- + Functions used by outputs ------------------------- diff --git a/docs/sphinx/reference-scenes.rst b/docs/sphinx/reference-scenes.rst index e01d8a0..c8faf3d 100644 --- a/docs/sphinx/reference-scenes.rst +++ b/docs/sphinx/reference-scenes.rst @@ -101,6 +101,23 @@ Scene Item Crop Structure (obs_sceneitem_crop) Bottom crop value. +Scene Item Order Info Structure (*obs_sceneitem_order_info) +---------------------------------------------- + +.. type:: struct obs_sceneitem_order_info + + Scene item order info structure. + +.. member:: obs_sceneitem_t *obs_sceneitem_order_info.group + + Specifies the group this scene item belongs to, or *NULL* if none. + +.. member:: obs_sceneitem_t *obs_sceneitem_order_info.item + + Specifies the scene item. + + + .. _scene_signal_reference: Scene Signals @@ -223,6 +240,12 @@ General Scene Functions --------------------- +.. function:: bool obs_scene_reorder_items2(obs_scene_t *scene, struct obs_sceneitem_order_info *item_order, size_t item_order_size) + + Reorders items within a scene with groups and group sub-items. + +--------------------- + .. _scene_item_reference: @@ -412,3 +435,119 @@ Scene Item Functions :return: An incremented reference to the private settings of the scene item. Allows the front-end to set custom information which is saved with the scene item + +--------------------- + + +.. _scene_item_group_reference: + +Scene Item Group Functions +-------------------------- + +.. function:: obs_sceneitem_t *obs_scene_add_group(obs_scene_t *scene, const char *name) + + Adds a group with the specified name. + + :param scene: Scene to add the group to + :param name: Name of the group + :return: The new group's scene item + +--------------------- + +.. function:: obs_sceneitem_t *obs_scene_insert_group(obs_scene_t *scene, const char *name, obs_sceneitem_t **items, size_t count) + + Creates a group out of the specified scene items. The group will be + inserted at the top scene item. + + :param scene: Scene to add the group to + :param name: Name of the group + :param items: Array of scene items to put in a group + :param count: Number of scene items in the array + :return: The new group's scene item + +--------------------- + +.. function:: obs_sceneitem_t *obs_scene_get_group(obs_scene_t *scene, const char *name) + + Finds a group within a scene by its name. + + :param scene: Scene to find the group within + :param name: The name of the group to find + :return: The group scene item, or *NULL* if not found + +--------------------- + +.. function:: bool obs_sceneitem_is_group(obs_sceneitem_t *item) + + :param item: Scene item + :return: *true* if scene item is a group, *false* otherwise + +--------------------- + +.. function:: obs_scene_t *obs_sceneitem_group_get_scene(const obs_sceneitem_t *group) + + :param group: Group scene item + :return: Scene of the group, or *NULL* if not a group + +--------------------- + +.. function:: void obs_sceneitem_group_ungroup(obs_sceneitem_t *group) + + Ungroups the specified group. Scene items within the group will be + placed where the group was. + +--------------------- + +.. function:: void obs_sceneitem_group_add_item(obs_sceneitem_t *group, obs_sceneitem_t *item) + + Adds a scene item to a group. + +--------------------- + +.. function:: void obs_sceneitem_group_remove_item(obs_sceneitem_t *item) + + Rmoves a scene item from a group. The item will be placed before the + group in the main scene. + +--------------------- + +.. function:: obs_sceneitem_t *obs_sceneitem_get_group(obs_sceneitem_t *item) + + Returns the parent group of a scene item. + + :param item: Scene item to get the group of + :return: The parent group of the scene item, or *NULL* if not in + a group + +--------------------- + +.. function:: obs_sceneitem_t *obs_sceneitem_group_from_scene(obs_scene_t *scene) + + :return: The group associated with the scene, or *NULL* if the + specified scene is not a group. + +--------------------- + +.. function:: obs_sceneitem_t *obs_sceneitem_group_from_source(obs_source_t *source) + + :return: The group associated with the scene's source, or *NULL* if + the specified source is not a group. + +--------------------- + +.. function:: void obs_sceneitem_group_enum_items(obs_sceneitem_t *group, bool (*callback)(obs_scene_t*, obs_sceneitem_t*, void*), void *param) + + Enumerates scene items within a group. + +--------------------- + +.. function:: void obs_sceneitem_defer_group_resize_begin(obs_sceneitem_t *item) +.. function:: void obs_sceneitem_defer_group_resize_end(obs_sceneitem_t *item) + + Allows the ability to call any one of the transform functions on + scene items within a group without updating the internal matrices of + the group until obs_sceneitem_defer_group_resize_end has been called. + + This is necessary if the user is resizing items while they are within + a group, as the group's transform will automatically update its + transform every frame otherwise. diff --git a/docs/sphinx/scripting.rst b/docs/sphinx/scripting.rst index f19f013..855abd7 100644 --- a/docs/sphinx/scripting.rst +++ b/docs/sphinx/scripting.rst @@ -144,7 +144,7 @@ and define its keys the same way you would define an return "My Source" end - info.create = function(source, settings) + info.create = function(settings, source) -- typically source data would be stored as a table local my_source_data = {} diff --git a/libobs-d3d11/d3d11-subsystem.cpp b/libobs-d3d11/d3d11-subsystem.cpp index 41f5730..aa335aa 100644 --- a/libobs-d3d11/d3d11-subsystem.cpp +++ b/libobs-d3d11/d3d11-subsystem.cpp @@ -166,6 +166,9 @@ gs_swap_chain::gs_swap_chain(gs_device *device, const gs_init_data *data) if (FAILED(hr)) throw HRError("Failed to create swap chain", hr); + /* Ignore Alt+Enter */ + device->factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER); + Init(); } diff --git a/libobs-opengl/gl-shaderparser.c b/libobs-opengl/gl-shaderparser.c index 61f19dc..2c607ac 100644 --- a/libobs-opengl/gl-shaderparser.c +++ b/libobs-opengl/gl-shaderparser.c @@ -108,6 +108,10 @@ static void gl_write_var(struct gl_shader_parser *glsp, struct shader_var *var) dstr_cat(&glsp->gl_string, "uniform "); else if (var->var_type == SHADER_VAR_CONST) dstr_cat(&glsp->gl_string, "const "); + else if (var->var_type == SHADER_VAR_INOUT) + dstr_cat(&glsp->gl_string, "inout "); + else if (var->var_type == SHADER_VAR_OUT) + dstr_cat(&glsp->gl_string, "out "); gl_write_type(glsp, var->type); dstr_cat(&glsp->gl_string, " "); diff --git a/libobs-opengl/gl-x11.c b/libobs-opengl/gl-x11.c index 2653cb1..697ef38 100644 --- a/libobs-opengl/gl-x11.c +++ b/libobs-opengl/gl-x11.c @@ -323,10 +323,16 @@ error: static int x_error_handler(Display *display, XErrorEvent *error) { - char str[512]; - XGetErrorText(display, error->error_code, str, sizeof(str)); + char str1[512]; + char str2[512]; + char str3[512]; + XGetErrorText(display, error->error_code, str1, sizeof(str1)); + XGetErrorText(display, error->request_code, str2, sizeof(str2)); + XGetErrorText(display, error->minor_code, str3, sizeof(str3)); - blog(LOG_ERROR, "X Error: %s", str); + blog(LOG_ERROR, "X Error: %s, Major opcode: %s, " + "Minor opcode: %s, Serial: %lu", + str1, str2, str3, error->serial); return 0; } @@ -514,8 +520,10 @@ extern void gl_getclientsize(const struct gs_swap_chain *swap, xcb_window_t window = swap->wi->window; xcb_get_geometry_reply_t *geometry = get_window_geometry(xcb_conn, window); - *width = geometry->width; - *height = geometry->height; + if (geometry) { + *width = geometry->width; + *height = geometry->height; + } free(geometry); } diff --git a/libobs/CMakeLists.txt b/libobs/CMakeLists.txt index 00c0ebc..bddfa5c 100644 --- a/libobs/CMakeLists.txt +++ b/libobs/CMakeLists.txt @@ -13,6 +13,13 @@ endif() if(UNIX) if (NOT APPLE) + find_package(X11_XCB REQUIRED) + find_package(XCB OPTIONAL_COMPONENTS XINPUT) + if (XCB_XINPUT_FOUND) + set(USE_XINPUT "1") + else() + set(USE_XINPUT "0") + endif() find_package(PulseAudio) if (NOT "${PULSEAUDIO_LIBRARY}" STREQUAL "") message(STATUS "Found PulseAudio - Audio Monitor enabled") @@ -22,14 +29,13 @@ if(UNIX) endif() else() set(HAVE_PULSEAUDIO "0") + set(USE_XINPUT "0") endif() find_package(DBus QUIET) - if (NOT APPLE) - find_package(X11_XCB REQUIRED) - endif() else() set(HAVE_DBUS "0") set(HAVE_PULSEAUDIO "0") + set(USE_XINPUT "0") endif() find_package(ImageMagick QUIET COMPONENTS MagickCore) @@ -199,6 +205,16 @@ elseif(UNIX) ${libobs_PLATFORM_DEPS} ${X11_XCB_LIBRARIES}) + if(USE_XINPUT) + include_directories( + ${XCB_XINPUT_INCLUDE_DIR}) + add_definitions( + ${XCB_DEFINITIONS}) + set(libobs_PLATFORM_DEPS + ${XCB_XINPUT_LIBRARY} + ${libobs_PLATFORM_DEPS}) + endif() + if(HAVE_PULSEAUDIO) set(libobs_PLATFORM_DEPS ${libobs_PLATFORM_DEPS} diff --git a/libobs/audio-monitoring/osx/coreaudio-enum-devices.c b/libobs/audio-monitoring/osx/coreaudio-enum-devices.c index 52be3f4..9f06cb6 100644 --- a/libobs/audio-monitoring/osx/coreaudio-enum-devices.c +++ b/libobs/audio-monitoring/osx/coreaudio-enum-devices.c @@ -25,23 +25,21 @@ static bool obs_enum_audio_monitoring_device(obs_enum_audio_device_cb cb, AudioObjectPropertyAddress addr = { kAudioDevicePropertyStreams, - kAudioDevicePropertyScopeInput, + kAudioDevicePropertyScopeOutput, kAudioObjectPropertyElementMaster }; - /* check to see if it's a mac input device */ - if (!allow_inputs) { - AudioObjectGetPropertyDataSize(id, &addr, 0, NULL, &size); - if (!size) - return true; - } + /* Check if the device is capable of audio output. */ + AudioObjectGetPropertyDataSize(id, &addr, 0, NULL, &size); + if (!allow_inputs && !size) + return true; size = sizeof(CFStringRef); addr.mSelector = kAudioDevicePropertyDeviceUID; stat = AudioObjectGetPropertyData(id, &addr, 0, NULL, &size, &cf_uid); if (!success(stat, "get audio device UID")) - return true; + goto fail; addr.mSelector = kAudioDevicePropertyDeviceNameCFString; stat = AudioObjectGetPropertyData(id, &addr, 0, NULL, &size, &cf_name); diff --git a/libobs/audio-monitoring/osx/coreaudio-output.c b/libobs/audio-monitoring/osx/coreaudio-output.c index 3e2bcd1..af2d7a0 100644 --- a/libobs/audio-monitoring/osx/coreaudio-output.c +++ b/libobs/audio-monitoring/osx/coreaudio-output.c @@ -1,5 +1,5 @@ #include -#include +#include #include #include @@ -193,14 +193,14 @@ static bool audio_monitor_init(struct audio_monitor *monitor, } if (strcmp(uid, "default") != 0) { - CFStringRef cf_uid = CFStringCreateWithBytesNoCopy(NULL, + CFStringRef cf_uid = CFStringCreateWithBytes(NULL, (const UInt8*)uid, strlen(uid), kCFStringEncodingUTF8, - false, NULL); + false); stat = AudioQueueSetProperty(monitor->queue, kAudioQueueProperty_CurrentDevice, - cf_uid, sizeof(cf_uid)); + &cf_uid, sizeof(cf_uid)); CFRelease(cf_uid); if (!success(stat, "set current device")) { diff --git a/libobs/callback/signal.c b/libobs/callback/signal.c index 5b7c3ee..d0f0a4d 100644 --- a/libobs/callback/signal.c +++ b/libobs/callback/signal.c @@ -24,6 +24,7 @@ struct signal_callback { signal_callback_t callback; void *data; bool remove; + bool keep_ref; }; struct signal_info { @@ -96,6 +97,7 @@ struct global_callback_info { struct signal_handler { struct signal_info *first; pthread_mutex_t mutex; + volatile long refs; DARRAY(struct global_callback_info) global_callbacks; pthread_mutex_t global_callbacks_mutex; @@ -126,6 +128,7 @@ signal_handler_t *signal_handler_create(void) { struct signal_handler *handler = bzalloc(sizeof(struct signal_handler)); handler->first = NULL; + handler->refs = 1; pthread_mutexattr_t attr; if (pthread_mutexattr_init(&attr) != 0) @@ -149,20 +152,25 @@ signal_handler_t *signal_handler_create(void) return handler; } +static void signal_handler_actually_destroy(signal_handler_t *handler) +{ + struct signal_info *sig = handler->first; + while (sig != NULL) { + struct signal_info *next = sig->next; + signal_info_destroy(sig); + sig = next; + } + + da_free(handler->global_callbacks); + pthread_mutex_destroy(&handler->global_callbacks_mutex); + pthread_mutex_destroy(&handler->mutex); + bfree(handler); +} + void signal_handler_destroy(signal_handler_t *handler) { - if (handler) { - struct signal_info *sig = handler->first; - while (sig != NULL) { - struct signal_info *next = sig->next; - signal_info_destroy(sig); - sig = next; - } - - da_free(handler->global_callbacks); - pthread_mutex_destroy(&handler->global_callbacks_mutex); - pthread_mutex_destroy(&handler->mutex); - bfree(handler); + if (handler && os_atomic_dec_long(&handler->refs) == 0) { + signal_handler_actually_destroy(handler); } } @@ -197,11 +205,12 @@ bool signal_handler_add(signal_handler_t *handler, const char *signal_decl) return success; } -void signal_handler_connect(signal_handler_t *handler, const char *signal, - signal_callback_t callback, void *data) +static void signal_handler_connect_internal(signal_handler_t *handler, + const char *signal, signal_callback_t callback, void *data, + bool keep_ref) { struct signal_info *sig, *last; - struct signal_callback cb_data = {callback, data, false}; + struct signal_callback cb_data = {callback, data, false, keep_ref}; size_t idx; if (!handler) @@ -221,13 +230,28 @@ void signal_handler_connect(signal_handler_t *handler, const char *signal, pthread_mutex_lock(&sig->mutex); + if (keep_ref) + os_atomic_inc_long(&handler->refs); + idx = signal_get_callback_idx(sig, callback, data); - if (idx == DARRAY_INVALID) + if (keep_ref || idx == DARRAY_INVALID) da_push_back(sig->callbacks, &cb_data); pthread_mutex_unlock(&sig->mutex); } +void signal_handler_connect(signal_handler_t *handler, const char *signal, + signal_callback_t callback, void *data) +{ + signal_handler_connect_internal(handler, signal, callback, data, false); +} + +void signal_handler_connect_ref(signal_handler_t *handler, const char *signal, + signal_callback_t callback, void *data) +{ + signal_handler_connect_internal(handler, signal, callback, data, true); +} + static inline struct signal_info *getsignal_locked(signal_handler_t *handler, const char *name) { @@ -247,6 +271,7 @@ void signal_handler_disconnect(signal_handler_t *handler, const char *signal, signal_callback_t callback, void *data) { struct signal_info *sig = getsignal_locked(handler, signal); + bool keep_ref = false; size_t idx; if (!sig) @@ -256,13 +281,19 @@ void signal_handler_disconnect(signal_handler_t *handler, const char *signal, idx = signal_get_callback_idx(sig, callback, data); if (idx != DARRAY_INVALID) { - if (sig->signalling) + if (sig->signalling) { sig->callbacks.array[idx].remove = true; - else + } else { + keep_ref = sig->callbacks.array[idx].keep_ref; da_erase(sig->callbacks, idx); + } } pthread_mutex_unlock(&sig->mutex); + + if (keep_ref && os_atomic_dec_long(&handler->refs) == 0) { + signal_handler_actually_destroy(handler); + } } static THREAD_LOCAL struct signal_callback *current_signal_cb = NULL; @@ -280,6 +311,7 @@ void signal_handler_signal(signal_handler_t *handler, const char *signal, calldata_t *params) { struct signal_info *sig = getsignal_locked(handler, signal); + long remove_refs = 0; if (!sig) return; @@ -298,8 +330,12 @@ void signal_handler_signal(signal_handler_t *handler, const char *signal, for (size_t i = sig->callbacks.num; i > 0; i--) { struct signal_callback *cb = sig->callbacks.array+i-1; - if (cb->remove) + if (cb->remove) { + if (cb->keep_ref) + remove_refs++; + da_erase(sig->callbacks, i-1); + } } sig->signalling = false; @@ -331,6 +367,12 @@ void signal_handler_signal(signal_handler_t *handler, const char *signal, } pthread_mutex_unlock(&handler->global_callbacks_mutex); + + if (remove_refs) { + os_atomic_set_long(&handler->refs, + os_atomic_load_long(&handler->refs) - + remove_refs); + } } void signal_handler_connect_global(signal_handler_t *handler, diff --git a/libobs/callback/signal.h b/libobs/callback/signal.h index ea4f0a9..e9f86f7 100644 --- a/libobs/callback/signal.h +++ b/libobs/callback/signal.h @@ -58,6 +58,8 @@ static inline bool signal_handler_add_array(signal_handler_t *handler, EXPORT void signal_handler_connect(signal_handler_t *handler, const char *signal, signal_callback_t callback, void *data); +EXPORT void signal_handler_connect_ref(signal_handler_t *handler, + const char *signal, signal_callback_t callback, void *data); EXPORT void signal_handler_disconnect(signal_handler_t *handler, const char *signal, signal_callback_t callback, void *data); diff --git a/libobs/graphics/effect-parser.c b/libobs/graphics/effect-parser.c index a2a931b..d721b41 100644 --- a/libobs/graphics/effect-parser.c +++ b/libobs/graphics/effect-parser.c @@ -491,13 +491,40 @@ static inline int ep_parse_func_param(struct effect_parser *ep, struct ep_func *func, struct ep_var *var) { int code; + bool var_type_keyword = false; if (!cf_next_valid_token(&ep->cfp)) return PARSE_EOF; - code = ep_check_for_keyword(ep, "uniform", &var->uniform); + code = ep_check_for_keyword(ep, "in", &var_type_keyword); if (code == PARSE_EOF) return PARSE_EOF; + else if (var_type_keyword) + var->var_type = EP_VAR_IN; + + if (!var_type_keyword) { + code = ep_check_for_keyword(ep, "inout", &var_type_keyword); + if (code == PARSE_EOF) + return PARSE_EOF; + else if (var_type_keyword) + var->var_type = EP_VAR_INOUT; + } + + if (!var_type_keyword) { + code = ep_check_for_keyword(ep, "out", &var_type_keyword); + if (code == PARSE_EOF) + return PARSE_EOF; + else if (var_type_keyword) + var->var_type = EP_VAR_OUT; + } + + if (!var_type_keyword) { + code = ep_check_for_keyword(ep, "uniform", &var_type_keyword); + if (code == PARSE_EOF) + return PARSE_EOF; + else if (var_type_keyword) + var->var_type = EP_VAR_UNIFORM; + } code = cf_get_name(&ep->cfp, &var->type, "type", ")"); if (code != PARSE_SUCCESS) @@ -1083,8 +1110,14 @@ static inline void ep_write_func_sampler_deps(struct effect_parser *ep, static inline void ep_write_var(struct dstr *shader, struct ep_var *var) { - if (var->uniform) + if (var->var_type == EP_VAR_INOUT) + dstr_cat(shader, "inout "); + else if (var->var_type == EP_VAR_OUT) + dstr_cat(shader, "out "); + else if (var->var_type == EP_VAR_UNIFORM) dstr_cat(shader, "uniform "); + // The "in" input modifier is implied by default, so leave it blank + // in that case. dstr_cat(shader, var->type); dstr_cat(shader, " "); diff --git a/libobs/graphics/effect-parser.h b/libobs/graphics/effect-parser.h index 36b6669..c0eb6f0 100644 --- a/libobs/graphics/effect-parser.h +++ b/libobs/graphics/effect-parser.h @@ -37,9 +37,17 @@ struct dstr; /* ------------------------------------------------------------------------- */ /* effect parser var data */ +enum ep_var_type { + EP_VAR_NONE, + EP_VAR_IN = EP_VAR_NONE, + EP_VAR_INOUT, + EP_VAR_OUT, + EP_VAR_UNIFORM +}; + struct ep_var { char *type, *name, *mapping; - bool uniform; + enum ep_var_type var_type; }; static inline void ep_var_init(struct ep_var *epv) diff --git a/libobs/graphics/shader-parser.c b/libobs/graphics/shader-parser.c index 5b17333..5464101 100644 --- a/libobs/graphics/shader-parser.c +++ b/libobs/graphics/shader-parser.c @@ -334,16 +334,40 @@ static inline int sp_parse_func_param(struct shader_parser *sp, struct shader_var *var) { int code; - bool is_uniform = false; + bool var_type_keyword = false; if (!cf_next_valid_token(&sp->cfp)) return PARSE_EOF; - code = sp_check_for_keyword(sp, "uniform", &is_uniform); + code = sp_check_for_keyword(sp, "in", &var_type_keyword); if (code == PARSE_EOF) return PARSE_EOF; + else if (var_type_keyword) + var->var_type = SHADER_VAR_IN; - var->var_type = is_uniform ? SHADER_VAR_UNIFORM : SHADER_VAR_NONE; + if (!var_type_keyword) { + code = sp_check_for_keyword(sp, "inout", &var_type_keyword); + if (code == PARSE_EOF) + return PARSE_EOF; + else if (var_type_keyword) + var->var_type = SHADER_VAR_INOUT; + } + + if (!var_type_keyword) { + code = sp_check_for_keyword(sp, "out", &var_type_keyword); + if (code == PARSE_EOF) + return PARSE_EOF; + else if (var_type_keyword) + var->var_type = SHADER_VAR_OUT; + } + + if (!var_type_keyword) { + code = sp_check_for_keyword(sp, "uniform", &var_type_keyword); + if (code == PARSE_EOF) + return PARSE_EOF; + else if (var_type_keyword) + var->var_type = SHADER_VAR_UNIFORM; + } code = cf_get_name(&sp->cfp, &var->type, "type", ")"); if (code != PARSE_SUCCESS) diff --git a/libobs/graphics/shader-parser.h b/libobs/graphics/shader-parser.h index 5e8f07c..8292904 100644 --- a/libobs/graphics/shader-parser.h +++ b/libobs/graphics/shader-parser.h @@ -38,6 +38,9 @@ EXPORT enum gs_address_mode get_address_mode(const char *address_mode); enum shader_var_type { SHADER_VAR_NONE, + SHADER_VAR_IN = SHADER_VAR_NONE, + SHADER_VAR_INOUT, + SHADER_VAR_OUT, SHADER_VAR_UNIFORM, SHADER_VAR_CONST }; diff --git a/libobs/media-io/media-remux.c b/libobs/media-io/media-remux.c index fc19bb5..9f5f72f 100644 --- a/libobs/media-io/media-remux.c +++ b/libobs/media-io/media-remux.c @@ -109,6 +109,8 @@ static inline bool init_output(media_remux_job_t job, const char *out_filename) } out_stream->time_base = out_stream->codec->time_base; + av_dict_copy(&out_stream->metadata, in_stream->metadata, 0); + out_stream->codec->codec_tag = 0; if (job->ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) out_stream->codec->flags |= CODEC_FLAG_GLOBAL_H; @@ -141,6 +143,9 @@ bool media_remux_job_create(media_remux_job_t *job, const char *in_filename, if (!os_file_exists(in_filename)) return false; + if (strcmp(in_filename, out_filename) == 0) + return false; + *job = (media_remux_job_t)bzalloc(sizeof(struct media_remux_job)); if (!*job) return false; diff --git a/libobs/obs-audio-controls.c b/libobs/obs-audio-controls.c index 4b0c79b..0de00c7 100644 --- a/libobs/obs-audio-controls.c +++ b/libobs/obs-audio-controls.c @@ -16,6 +16,7 @@ along with this program. If not, see . */ #include +#include #include "util/threading.h" #include "util/bmem.h" @@ -62,18 +63,20 @@ struct meter_cb { }; struct obs_volmeter { - pthread_mutex_t mutex; - obs_source_t *source; - enum obs_fader_type type; - float cur_db; + pthread_mutex_t mutex; + obs_source_t *source; + enum obs_fader_type type; + float cur_db; - pthread_mutex_t callback_mutex; - DARRAY(struct meter_cb)callbacks; + pthread_mutex_t callback_mutex; + DARRAY(struct meter_cb) callbacks; - unsigned int update_ms; + enum obs_peak_meter_type peak_meter_type; + unsigned int update_ms; + float prev_samples[MAX_AUDIO_CHANNELS][4]; - float vol_magnitude[MAX_AUDIO_CHANNELS]; - float vol_peak[MAX_AUDIO_CHANNELS]; + float magnitude[MAX_AUDIO_CHANNELS]; + float peak[MAX_AUDIO_CHANNELS]; }; static float cubic_def_to_db(const float def) @@ -256,48 +259,257 @@ static void volmeter_source_destroyed(void *vptr, calldata_t *calldata) obs_volmeter_detach_source(volmeter); } -static void volmeter_process_audio_data(obs_volmeter_t *volmeter, - const struct audio_data *data) +static int get_nr_channels_from_audio_data(const struct audio_data *data) +{ + int nr_channels = 0; + for (int i = 0; i < MAX_AV_PLANES; i++) { + if (data->data[i]) + nr_channels++; + } + return CLAMP(nr_channels, 0, MAX_AUDIO_CHANNELS); +} + +/* msb(h, g, f, e) lsb(d, c, b, a) --> msb(h, h, g, f) lsb(e, d, c, b) + */ +#define SHIFT_RIGHT_2PS(msb, lsb) {\ + __m128 tmp = _mm_shuffle_ps(lsb, msb, _MM_SHUFFLE(0, 0, 3, 3));\ + lsb = _mm_shuffle_ps(lsb, tmp, _MM_SHUFFLE(2, 1, 2, 1));\ + msb = _mm_shuffle_ps(msb, msb, _MM_SHUFFLE(3, 3, 2, 1));\ +} + +/* x(d, c, b, a) --> (|d|, |c|, |b|, |a|) + */ +#define abs_ps(v) \ + _mm_andnot_ps(_mm_set1_ps(-0.f), v) + +/* Take cross product of a vector with a matrix resulting in vector. + */ +#define VECTOR_MATRIX_CROSS_PS(out, v, m0, m1, m2, m3) \ +{\ + out = _mm_mul_ps(v, m0);\ + __m128 mul1 = _mm_mul_ps(v, m1);\ + __m128 mul2 = _mm_mul_ps(v, m2);\ + __m128 mul3 = _mm_mul_ps(v, m3);\ +\ + _MM_TRANSPOSE4_PS(out, mul1, mul2, mul3);\ +\ + out = _mm_add_ps(out, mul1);\ + out = _mm_add_ps(out, mul2);\ + out = _mm_add_ps(out, mul3);\ +} + +/* x4(d, c, b, a) --> max(a, b, c, d) + */ +#define hmax_ps(r, x4) \ + do { \ + float x4_mem[4]; \ + _mm_storeu_ps(x4_mem, x4); \ + r = x4_mem[0]; \ + r = fmaxf(r, x4_mem[1]); \ + r = fmaxf(r, x4_mem[2]); \ + r = fmaxf(r, x4_mem[3]); \ + } while (false) + +/* Calculate the true peak over a set of samples. + * The algorithm implements 5x oversampling by using Whittaker–Shannon + * interpolation over four samples. + * + * The four samples have location t=-1.5, -0.5, +0.5, +1.5 + * The oversamples are taken at locations t=-0.3, -0.1, +0.1, +0.3 + * + * @param previous_samples Last 4 samples from the previous iteration. + * @param samples The samples to find the peak in. + * @param nr_samples Number of sets of 4 samples. + * @returns 5 times oversampled true-peak from the set of samples. + */ +static float get_true_peak(__m128 previous_samples, const float *samples, + size_t nr_samples) +{ + /* These are normalized-sinc parameters for interpolating over sample + * points which are located at x-coords: -1.5, -0.5, +0.5, +1.5. + * And oversample points at x-coords: -0.3, -0.1, 0.1, 0.3. */ + const __m128 m3 = _mm_set_ps(-0.155915f, 0.935489f, 0.233872f, -0.103943f); + const __m128 m1 = _mm_set_ps(-0.216236f, 0.756827f, 0.504551f, -0.189207f); + const __m128 p1 = _mm_set_ps(-0.189207f, 0.504551f, 0.756827f, -0.216236f); + const __m128 p3 = _mm_set_ps(-0.103943f, 0.233872f, 0.935489f, -0.155915f); + + __m128 work = previous_samples; + __m128 peak = previous_samples; + for (size_t i = 0; (i + 3) < nr_samples; i += 4) { + __m128 new_work = _mm_load_ps(&samples[i]); + __m128 intrp_samples; + + /* Include the actual sample values in the peak. */ + __m128 abs_new_work = abs_ps(new_work); + peak = _mm_max_ps(peak, abs_new_work); + + /* Shift in the next point. */ + SHIFT_RIGHT_2PS(new_work, work); + VECTOR_MATRIX_CROSS_PS(intrp_samples, work, m3, m1, p1, p3); + peak = _mm_max_ps(peak, abs_ps(intrp_samples)); + + SHIFT_RIGHT_2PS(new_work, work); + VECTOR_MATRIX_CROSS_PS(intrp_samples, work, m3, m1, p1, p3); + peak = _mm_max_ps(peak, abs_ps(intrp_samples)); + + SHIFT_RIGHT_2PS(new_work, work); + VECTOR_MATRIX_CROSS_PS(intrp_samples, work, m3, m1, p1, p3); + peak = _mm_max_ps(peak, abs_ps(intrp_samples)); + + SHIFT_RIGHT_2PS(new_work, work); + VECTOR_MATRIX_CROSS_PS(intrp_samples, work, m3, m1, p1, p3); + peak = _mm_max_ps(peak, abs_ps(intrp_samples)); + } + + float r; + hmax_ps(r, peak); + return r; +} + +/* points contain the first four samples to calculate the sinc interpolation + * over. They will have come from a previous iteration. + */ +static float get_sample_peak(__m128 previous_samples, const float *samples, + size_t nr_samples) +{ + __m128 peak = previous_samples; + for (size_t i = 0; (i + 3) < nr_samples; i += 4) { + __m128 new_work = _mm_load_ps(&samples[i]); + peak = _mm_max_ps(peak, abs_ps(new_work)); + } + + float r; + hmax_ps(r, peak); + return r; +} + +static void volmeter_process_peak_last_samples(obs_volmeter_t *volmeter, + int channel_nr, float *samples, size_t nr_samples) +{ + /* Take the last 4 samples that need to be used for the next peak + * calculation. If there are less than 4 samples in total the new + * samples shift out the old samples. */ + + switch (nr_samples) { + case 0: + break; + case 1: + volmeter->prev_samples[channel_nr][0] = + volmeter->prev_samples[channel_nr][1]; + volmeter->prev_samples[channel_nr][1] = + volmeter->prev_samples[channel_nr][2]; + volmeter->prev_samples[channel_nr][2] = + volmeter->prev_samples[channel_nr][3]; + volmeter->prev_samples[channel_nr][3] = samples[nr_samples-1]; + break; + case 2: + volmeter->prev_samples[channel_nr][0] = + volmeter->prev_samples[channel_nr][2]; + volmeter->prev_samples[channel_nr][1] = + volmeter->prev_samples[channel_nr][3]; + volmeter->prev_samples[channel_nr][2] = samples[nr_samples-2]; + volmeter->prev_samples[channel_nr][3] = samples[nr_samples-1]; + break; + case 3: + volmeter->prev_samples[channel_nr][0] = + volmeter->prev_samples[channel_nr][3]; + volmeter->prev_samples[channel_nr][1] = samples[nr_samples-3]; + volmeter->prev_samples[channel_nr][2] = samples[nr_samples-2]; + volmeter->prev_samples[channel_nr][3] = samples[nr_samples-1]; + break; + default: + volmeter->prev_samples[channel_nr][0] = samples[nr_samples-4]; + volmeter->prev_samples[channel_nr][1] = samples[nr_samples-3]; + volmeter->prev_samples[channel_nr][2] = samples[nr_samples-2]; + volmeter->prev_samples[channel_nr][3] = samples[nr_samples-1]; + } +} + +static void volmeter_process_peak(obs_volmeter_t *volmeter, + const struct audio_data *data, int nr_channels) { int nr_samples = data->frames; int channel_nr = 0; - - for (size_t plane_nr = 0; plane_nr < MAX_AV_PLANES; plane_nr++) { + for (int plane_nr = 0; channel_nr < nr_channels; plane_nr++) { float *samples = (float *)data->data[plane_nr]; if (!samples) { - // This plane does not contain data. + continue; + } + if (((uintptr_t)samples & 0xf) > 0) { + printf("Audio plane %i is not aligned %p skipping " + "peak volume measurement.\n", + plane_nr, samples); + volmeter->peak[channel_nr] = 1.0; + channel_nr++; continue; } - // For each plane calculate: - // * peak = the maximum-absolute of the sample values. - // * magnitude = root-mean-square of the sample values. - // A VU meter needs to integrate over 300ms, but this will - // be handled by the ballistics of the meter itself, - // reality. Which makes this calculation independent of - // sample rate or update rate. - float peak = 0.0; - float sum_of_squares = 0.0; - for (int sample_nr = 0; sample_nr < nr_samples; sample_nr++) { - float sample = samples[sample_nr]; + /* volmeter->prev_samples may not be aligned to 16 bytes; + * use unaligned load. */ + __m128 previous_samples = _mm_loadu_ps( + volmeter->prev_samples[channel_nr]); + + float peak; + switch (volmeter->peak_meter_type) { + case TRUE_PEAK_METER: + peak = get_true_peak(previous_samples, samples, + nr_samples); + break; + + case SAMPLE_PEAK_METER: + default: + peak = get_sample_peak(previous_samples, samples, + nr_samples); + break; - peak = fmaxf(peak, fabsf(sample)); - sum_of_squares += (sample * sample); } - volmeter->vol_magnitude[channel_nr] = sqrtf(sum_of_squares / + volmeter_process_peak_last_samples(volmeter, channel_nr, samples, nr_samples); - volmeter->vol_peak[channel_nr] = peak; + + volmeter->peak[channel_nr] = peak; + channel_nr++; } - // Clear audio channels that are not in use. + /* Clear the peak of the channels that have not been handled. */ for (; channel_nr < MAX_AUDIO_CHANNELS; channel_nr++) { - volmeter->vol_magnitude[channel_nr] = 0.0; - volmeter->vol_peak[channel_nr] = 0.0; + volmeter->peak[channel_nr] = 0.0; } } +static void volmeter_process_magnitude(obs_volmeter_t *volmeter, + const struct audio_data *data, int nr_channels) +{ + size_t nr_samples = data->frames; + + int channel_nr = 0; + for (int plane_nr = 0; channel_nr < nr_channels; plane_nr++) { + float *samples = (float *)data->data[plane_nr]; + if (!samples) { + continue; + } + + float sum = 0.0; + for (size_t i = 0; i < nr_samples; i++) { + float sample = samples[i]; + sum += sample * sample; + } + volmeter->magnitude[channel_nr] = sqrtf(sum / nr_samples); + + channel_nr++; + } +} + +static void volmeter_process_audio_data(obs_volmeter_t *volmeter, + const struct audio_data *data) +{ + int nr_channels = get_nr_channels_from_audio_data(data); + + volmeter_process_peak(volmeter, data, nr_channels); + volmeter_process_magnitude(volmeter, data, nr_channels); +} + static void volmeter_source_data_received(void *vptr, obs_source_t *source, const struct audio_data *data, bool muted) { @@ -317,15 +529,15 @@ static void volmeter_source_data_received(void *vptr, obs_source_t *source, for (int channel_nr = 0; channel_nr < MAX_AUDIO_CHANNELS; channel_nr++) { magnitude[channel_nr] = mul_to_db( - volmeter->vol_magnitude[channel_nr] * mul); + volmeter->magnitude[channel_nr] * mul); peak[channel_nr] = mul_to_db( - volmeter->vol_peak[channel_nr] * mul); - input_peak[channel_nr] = mul_to_db( - volmeter->vol_peak[channel_nr]); - } + volmeter->peak[channel_nr] * mul); - // The input-peak is NOT adjusted with volume, so that the user - // can check the input-gain. + /* The input-peak is NOT adjusted with volume, so that the user + * can check the input-gain. */ + input_peak[channel_nr] = mul_to_db( + volmeter->peak[channel_nr]); + } pthread_mutex_unlock(&volmeter->mutex); @@ -641,6 +853,14 @@ void obs_volmeter_detach_source(obs_volmeter_t *volmeter) volmeter_source_data_received, volmeter); } +void obs_volmeter_set_peak_meter_type(obs_volmeter_t *volmeter, + enum obs_peak_meter_type peak_meter_type) +{ + pthread_mutex_lock(&volmeter->mutex); + volmeter->peak_meter_type = peak_meter_type; + pthread_mutex_unlock(&volmeter->mutex); +} + void obs_volmeter_set_update_interval(obs_volmeter_t *volmeter, const unsigned int ms) { diff --git a/libobs/obs-audio-controls.h b/libobs/obs-audio-controls.h index 158af7b..384d618 100644 --- a/libobs/obs-audio-controls.h +++ b/libobs/obs-audio-controls.h @@ -68,6 +68,27 @@ enum obs_fader_type { OBS_FADER_LOG }; +/** + * @brief Peak meter types + */ +enum obs_peak_meter_type { + /** + * @brief A simple peak meter measuring the maximum of all samples. + * + * This was a very common type of peak meter used for audio, but + * is not very accurate with regards to further audio processing. + */ + SAMPLE_PEAK_METER, + + /** + * @brief An accurate peak meter measure the maximum of inter-samples. + * + * This meter is more computational intensive due to 4x oversampling + * to determine the true peak to an accuracy of +/- 0.5 dB. + */ + TRUE_PEAK_METER +}; + /** * @brief Create a fader * @param type the type of the fader @@ -200,6 +221,14 @@ EXPORT bool obs_volmeter_attach_source(obs_volmeter_t *volmeter, */ EXPORT void obs_volmeter_detach_source(obs_volmeter_t *volmeter); +/** + * @brief Set the peak meter type for the volume meter + * @param volmeter pointer to the volume meter object + * @param peak_meter_type set if true-peak needs to be measured. + */ +EXPORT void obs_volmeter_set_peak_meter_type(obs_volmeter_t *volmeter, + enum obs_peak_meter_type peak_meter_type); + /** * @brief Set the update interval for the volume meter * @param volmeter pointer to the volume meter object diff --git a/libobs/obs-config.h b/libobs/obs-config.h index 52b95cf..b6d8dc8 100644 --- a/libobs/obs-config.h +++ b/libobs/obs-config.h @@ -27,21 +27,21 @@ /* * Increment if major breaking API changes */ -#define LIBOBS_API_MAJOR_VER 21 +#define LIBOBS_API_MAJOR_VER 22 /* * Increment if backward-compatible additions * * Reset to zero each major version */ -#define LIBOBS_API_MINOR_VER 1 +#define LIBOBS_API_MINOR_VER 0 /* * Increment if backward-compatible bug fix * * Reset to zero each major or minor version */ -#define LIBOBS_API_PATCH_VER 2 +#define LIBOBS_API_PATCH_VER 3 #define MAKE_SEMANTIC_VERSION(major, minor, patch) \ ((major << 24) | \ @@ -61,6 +61,11 @@ # define OBS_INSTALL_PREFIX "" # define OBS_PLUGIN_DESTINATION "obs-plugins" # define OBS_RELATIVE_PREFIX "../../" +# define OBS_RELEASE_CANDIDATE_MAJOR 0 +# define OBS_RELEASE_CANDIDATE_MINOR 0 +# define OBS_RELEASE_CANDIDATE_PATCH 0 +# define OBS_RELEASE_CANDIDATE_VER 0 +# define OBS_RELEASE_CANDIDATE 0 #endif #define OBS_INSTALL_DATA_PATH OBS_INSTALL_PREFIX OBS_DATA_PATH diff --git a/libobs/obs-data.c b/libobs/obs-data.c index 870de33..676431d 100644 --- a/libobs/obs-data.c +++ b/libobs/obs-data.c @@ -1319,6 +1319,19 @@ void obs_data_array_insert(obs_data_array_t *array, size_t idx, obs_data_t *obj) da_insert(array->objects, idx, &obj); } +void obs_data_array_push_back_array(obs_data_array_t *array, + obs_data_array_t *array2) +{ + if (!array || !array2) + return; + + for (size_t i = 0; i < array2->objects.num; i++) { + obs_data_t *obj = array2->objects.array[i]; + obs_data_addref(obj); + } + da_push_back_da(array->objects, array2->objects); +} + void obs_data_array_erase(obs_data_array_t *array, size_t idx) { if (array) { diff --git a/libobs/obs-data.h b/libobs/obs-data.h index 824d23a..c0bbcbe 100644 --- a/libobs/obs-data.h +++ b/libobs/obs-data.h @@ -159,6 +159,8 @@ EXPORT obs_data_t *obs_data_array_item(obs_data_array_t *array, size_t idx); EXPORT size_t obs_data_array_push_back(obs_data_array_t *array, obs_data_t *obj); EXPORT void obs_data_array_insert(obs_data_array_t *array, size_t idx, obs_data_t *obj); +EXPORT void obs_data_array_push_back_array(obs_data_array_t *array, + obs_data_array_t *array2); EXPORT void obs_data_array_erase(obs_data_array_t *array, size_t idx); /* ------------------------------------------------------------------------- */ diff --git a/libobs/obs-encoder.c b/libobs/obs-encoder.c index e4d1320..83b2d7f 100644 --- a/libobs/obs-encoder.c +++ b/libobs/obs-encoder.c @@ -189,8 +189,7 @@ static void add_connection(struct obs_encoder *encoder) struct video_scale_info info = {0}; get_video_info(encoder, &info); - video_output_connect(encoder->media, &info, receive_video, - encoder); + start_raw_video(encoder->media, &info, receive_video, encoder); } set_encoder_active(encoder, true); @@ -202,8 +201,7 @@ static void remove_connection(struct obs_encoder *encoder) audio_output_disconnect(encoder->media, encoder->mixer_idx, receive_audio, encoder); else - video_output_disconnect(encoder->media, receive_video, - encoder); + stop_raw_video(encoder->media, receive_video, encoder); obs_encoder_shutdown(encoder); set_encoder_active(encoder, false); @@ -295,6 +293,14 @@ obs_data_t *obs_encoder_defaults(const char *id) return (info) ? get_defaults(info) : NULL; } +obs_data_t *obs_encoder_get_defaults(const obs_encoder_t *encoder) +{ + if (!obs_encoder_valid(encoder, "obs_encoder_defaults")) + return NULL; + + return get_defaults(&encoder->info); +} + obs_properties_t *obs_get_encoder_properties(const char *id) { const struct obs_encoder_info *ei = find_encoder(id); diff --git a/libobs/obs-hotkey.c b/libobs/obs-hotkey.c index 7d41cff..b150d29 100644 --- a/libobs/obs-hotkey.c +++ b/libobs/obs-hotkey.c @@ -79,6 +79,62 @@ obs_hotkey_t *obs_hotkey_binding_get_hotkey(obs_hotkey_binding_t *binding) return binding->hotkey; } +static inline bool find_id(obs_hotkey_id id, size_t *idx); +void obs_hotkey_set_name(obs_hotkey_id id, const char *name) +{ + size_t idx; + + if (!find_id(id, &idx)) + return; + + obs_hotkey_t *hotkey = &obs->hotkeys.hotkeys.array[idx]; + bfree(hotkey->name); + hotkey->name = bstrdup(name); +} + +void obs_hotkey_set_description(obs_hotkey_id id, const char *desc) +{ + size_t idx; + + if (!find_id(id, &idx)) + return; + + obs_hotkey_t *hotkey = &obs->hotkeys.hotkeys.array[idx]; + bfree(hotkey->description); + hotkey->description = bstrdup(desc); +} + +static inline bool find_pair_id(obs_hotkey_pair_id id, size_t *idx); +void obs_hotkey_pair_set_names(obs_hotkey_pair_id id, + const char *name0, const char *name1) +{ + size_t idx; + obs_hotkey_pair_t pair; + + if (!find_pair_id(id, &idx)) + return; + + pair = obs->hotkeys.hotkey_pairs.array[idx]; + + obs_hotkey_set_name(pair.id[0], name0); + obs_hotkey_set_name(pair.id[1], name1); +} + +void obs_hotkey_pair_set_descriptions(obs_hotkey_pair_id id, + const char *desc0, const char *desc1) +{ + size_t idx; + obs_hotkey_pair_t pair; + + if (!find_pair_id(id, &idx)) + return; + + pair = obs->hotkeys.hotkey_pairs.array[idx]; + + obs_hotkey_set_description(pair.id[0], desc0); + obs_hotkey_set_description(pair.id[1], desc1); +} + static void hotkey_signal(const char *signal, obs_hotkey_t *hotkey) { calldata_t data; @@ -804,6 +860,30 @@ obs_data_array_t *obs_hotkey_save(obs_hotkey_id id) return result; } +void obs_hotkey_pair_save(obs_hotkey_pair_id id, + obs_data_array_t **p_data0, + obs_data_array_t **p_data1) +{ + if ((!p_data0 && !p_data1) || !lock()) + return; + + size_t idx; + if (!find_pair_id(id, &idx)) + goto unlock; + + obs_hotkey_pair_t *pair = &obs->hotkeys.hotkey_pairs.array[idx]; + + if (p_data0 && find_id(pair->id[0], &idx)) { + *p_data0 = save_hotkey(&obs->hotkeys.hotkeys.array[idx]); + } + if (p_data1 && find_id(pair->id[1], &idx)) { + *p_data1 = save_hotkey(&obs->hotkeys.hotkeys.array[idx]); + } + +unlock: + unlock(); +} + static inline bool enum_save_hotkey(void *data, size_t idx, obs_hotkey_t *hotkey) { diff --git a/libobs/obs-hotkey.h b/libobs/obs-hotkey.h index 0385d65..4e4a86a 100644 --- a/libobs/obs-hotkey.h +++ b/libobs/obs-hotkey.h @@ -32,6 +32,8 @@ const size_t OBS_INVALID_HOTKEY_ID = (size_t)-1; const size_t OBS_INVALID_HOTKEY_PAIR_ID = (size_t)-1; #endif +#define XINPUT_MOUSE_LEN 33 + enum obs_key { #define OBS_HOTKEY(x) x, #include "obs-hotkeys.h" @@ -58,6 +60,8 @@ enum obs_hotkey_registerer_type { }; typedef enum obs_hotkey_registerer_type obs_hotkey_registerer_t; +/* getter functions */ + EXPORT obs_hotkey_id obs_hotkey_get_id(const obs_hotkey_t *key); EXPORT const char *obs_hotkey_get_name(const obs_hotkey_t *key); EXPORT const char *obs_hotkey_get_description(const obs_hotkey_t *key); @@ -74,6 +78,15 @@ EXPORT obs_hotkey_id obs_hotkey_binding_get_hotkey_id( EXPORT obs_hotkey_t *obs_hotkey_binding_get_hotkey( obs_hotkey_binding_t *binding); +/* setter functions */ + +EXPORT void obs_hotkey_set_name(obs_hotkey_id id, const char *name); +EXPORT void obs_hotkey_set_description(obs_hotkey_id id, const char *desc); +EXPORT void obs_hotkey_pair_set_names(obs_hotkey_pair_id id, + const char *name0, const char *name1); +EXPORT void obs_hotkey_pair_set_descriptions(obs_hotkey_pair_id id, + const char *desc0, const char *desc1); + #ifndef SWIG struct obs_hotkeys_translations { const char *insert; @@ -223,6 +236,10 @@ EXPORT void obs_hotkey_pair_load(obs_hotkey_pair_id id, obs_data_array_t *data0, EXPORT obs_data_array_t *obs_hotkey_save(obs_hotkey_id id); +EXPORT void obs_hotkey_pair_save(obs_hotkey_pair_id id, + obs_data_array_t **p_data0, + obs_data_array_t **p_data1); + EXPORT obs_data_t *obs_hotkeys_save_encoder(obs_encoder_t *encoder); EXPORT obs_data_t *obs_hotkeys_save_output(obs_output_t *output); diff --git a/libobs/obs-internal.h b/libobs/obs-internal.h index eede65b..b317fd9 100644 --- a/libobs/obs-internal.h +++ b/libobs/obs-internal.h @@ -248,6 +248,7 @@ struct obs_core_video { gs_samplerstate_t *point_sampler; gs_stagesurf_t *mapped_surface; int cur_texture; + long raw_active; uint64_t video_time; uint64_t video_avg_frame_time_ns; @@ -330,6 +331,8 @@ struct obs_core_data { long long unnamed_index; + obs_data_t *private_data; + volatile bool valid; }; @@ -408,6 +411,13 @@ extern bool audio_callback(void *param, uint64_t start_ts_in, uint64_t end_ts_in, uint64_t *out_ts, uint32_t mixers, struct audio_output_data *mixes); +extern void start_raw_video(video_t *video, + const struct video_scale_info *conversion, + void (*callback)(void *param, struct video_data *frame), + void *param); +extern void stop_raw_video(video_t *video, + void (*callback)(void *param, struct video_data *frame), + void *param); /* ------------------------------------------------------------------------- */ /* obs shared context data */ @@ -696,9 +706,6 @@ extern bool obs_source_init_context(struct obs_source *source, obs_data_t *settings, const char *name, obs_data_t *hotkey_data, bool private); -extern void obs_source_save(obs_source_t *source); -extern void obs_source_load(obs_source_t *source); - extern bool obs_transition_init(obs_source_t *transition); extern void obs_transition_free(obs_source_t *transition); extern void obs_transition_tick(obs_source_t *transition); diff --git a/libobs/obs-module.c b/libobs/obs-module.c index 274f9fe..4136965 100644 --- a/libobs/obs-module.c +++ b/libobs/obs-module.c @@ -85,6 +85,17 @@ int obs_open_module(obs_module_t **module, const char *path, if (!module || !path || !obs) return MODULE_ERROR; +#ifdef __APPLE__ + /* HACK: Do not load obsolete obs-browser build on macOS; the + * obs-browser plugin used to live in the Application Support + * directory. */ + if (astrstri(path, "Library/Application Support") != NULL && + astrstri(path, "obs-browser") != NULL) { + blog(LOG_WARNING, "Ignoring old obs-browser.so version"); + return MODULE_ERROR; + } +#endif + blog(LOG_DEBUG, "---------------------------------"); mod.module = os_dlopen(path); @@ -725,13 +736,13 @@ error: HANDLE_ERROR(size, obs_service_info, info); } -void obs_regsiter_modal_ui_s(const struct obs_modal_ui *info, size_t size) +void obs_register_modal_ui_s(const struct obs_modal_ui *info, size_t size) { #define CHECK_REQUIRED_VAL_(info, val, func) \ CHECK_REQUIRED_VAL(struct obs_modal_ui, info, val, func) - CHECK_REQUIRED_VAL_(info, task, obs_regsiter_modal_ui); - CHECK_REQUIRED_VAL_(info, target, obs_regsiter_modal_ui); - CHECK_REQUIRED_VAL_(info, exec, obs_regsiter_modal_ui); + CHECK_REQUIRED_VAL_(info, task, obs_register_modal_ui); + CHECK_REQUIRED_VAL_(info, target, obs_register_modal_ui); + CHECK_REQUIRED_VAL_(info, exec, obs_register_modal_ui); #undef CHECK_REQUIRED_VAL_ REGISTER_OBS_DEF(size, obs_modal_ui, obs->modal_ui_callbacks, info); @@ -741,13 +752,13 @@ error: HANDLE_ERROR(size, obs_modal_ui, info); } -void obs_regsiter_modeless_ui_s(const struct obs_modeless_ui *info, size_t size) +void obs_register_modeless_ui_s(const struct obs_modeless_ui *info, size_t size) { #define CHECK_REQUIRED_VAL_(info, val, func) \ CHECK_REQUIRED_VAL(struct obs_modeless_ui, info, val, func) - CHECK_REQUIRED_VAL_(info, task, obs_regsiter_modeless_ui); - CHECK_REQUIRED_VAL_(info, target, obs_regsiter_modeless_ui); - CHECK_REQUIRED_VAL_(info, create, obs_regsiter_modeless_ui); + CHECK_REQUIRED_VAL_(info, task, obs_register_modeless_ui); + CHECK_REQUIRED_VAL_(info, target, obs_register_modeless_ui); + CHECK_REQUIRED_VAL_(info, create, obs_register_modeless_ui); #undef CHECK_REQUIRED_VAL_ REGISTER_OBS_DEF(size, obs_modeless_ui, obs->modeless_ui_callbacks, diff --git a/libobs/obs-nix.c b/libobs/obs-nix.c index e20e82e..355b832 100644 --- a/libobs/obs-nix.c +++ b/libobs/obs-nix.c @@ -16,6 +16,7 @@ along with this program. If not, see . ******************************************************************************/ +#include "obs-internal.h" #if defined(__FreeBSD__) #define _GNU_SOURCE #endif @@ -28,13 +29,13 @@ #include #include #include +#if USE_XINPUT +#include +#endif #include #include #include -#include #include -#include "util/dstr.h" -#include "obs-internal.h" const char *get_module_extension(void) { @@ -236,6 +237,34 @@ static void log_kernel_version(void) blog(LOG_INFO, "Kernel Version: %s %s", info.sysname, info.release); } +static void log_x_info(void) +{ + Display *dpy = XOpenDisplay(NULL); + if (!dpy) { + blog(LOG_INFO, "Unable to open X display"); + return; + } + + int protocol_version = ProtocolVersion(dpy); + int protocol_revision = ProtocolRevision(dpy); + int vendor_release = VendorRelease(dpy); + const char *vendor_name = ServerVendor(dpy); + + if (strstr(vendor_name, "X.Org")) { + blog(LOG_INFO, "Window System: X%d.%d, Vendor: %s, Version: %d" + ".%d.%d", protocol_version, protocol_revision, + vendor_name, vendor_release / 10000000, + (vendor_release / 100000) % 100, + (vendor_release / 1000) % 100); + } else { + blog(LOG_INFO, "Window System: X%d.%d - vendor string: %s - " + "vendor release: %d", protocol_version, + protocol_revision, vendor_name, vendor_release); + } + + XCloseDisplay(dpy); +} + #if defined(__linux__) static void log_distribution_info(void) { @@ -292,6 +321,7 @@ void log_system_info(void) #if defined(__linux__) log_distribution_info(); #endif + log_x_info(); } /* So here's how linux works with key mapping: @@ -327,6 +357,12 @@ struct obs_hotkeys_platform { xcb_keysym_t *keysyms; int num_keysyms; int syms_per_code; + +#if USE_XINPUT + bool pressed[XINPUT_MOUSE_LEN]; + bool update[XINPUT_MOUSE_LEN]; + bool button_pressed[XINPUT_MOUSE_LEN]; +#endif }; #define MOUSE_1 (1<<16) @@ -707,6 +743,54 @@ error1: return error != NULL || reply == NULL; } +static xcb_screen_t *default_screen(obs_hotkeys_platform_t *context, + xcb_connection_t *connection) +{ + int def_screen_idx = XDefaultScreen(context->display); + xcb_screen_iterator_t iter; + + iter = xcb_setup_roots_iterator(xcb_get_setup(connection)); + while (iter.rem) { + if (def_screen_idx-- == 0) + return iter.data; + + xcb_screen_next(&iter); + } + + return NULL; +} + +static inline xcb_window_t root_window(obs_hotkeys_platform_t *context, + xcb_connection_t *connection) +{ + xcb_screen_t *screen = default_screen(context, connection); + if (screen) + return screen->root; + return 0; +} + +#if USE_XINPUT +static inline void registerMouseEvents(struct obs_core_hotkeys *hotkeys) +{ + obs_hotkeys_platform_t *context = hotkeys->platform_context; + xcb_connection_t *connection = XGetXCBConnection( + context->display); + xcb_window_t window = root_window(context, connection); + + struct { + xcb_input_event_mask_t head; + xcb_input_xi_event_mask_t mask; + } mask; + mask.head.deviceid = XCB_INPUT_DEVICE_ALL_MASTER; + mask.head.mask_len = sizeof(mask.mask) / sizeof(uint32_t); + mask.mask = XCB_INPUT_XI_EVENT_MASK_RAW_BUTTON_PRESS | + XCB_INPUT_XI_EVENT_MASK_RAW_BUTTON_RELEASE; + + xcb_input_xi_select_events(connection, window, 1, &mask.head); + xcb_flush(connection); +} +#endif + bool obs_hotkeys_platform_init(struct obs_core_hotkeys *hotkeys) { Display *display = XOpenDisplay(NULL); @@ -716,6 +800,9 @@ bool obs_hotkeys_platform_init(struct obs_core_hotkeys *hotkeys) hotkeys->platform_context = bzalloc(sizeof(obs_hotkeys_platform_t)); hotkeys->platform_context->display = display; +#if USE_XINPUT + registerMouseEvents(hotkeys); +#endif fill_base_keysyms(hotkeys); fill_keycodes(hotkeys); return true; @@ -735,41 +822,147 @@ void obs_hotkeys_platform_free(struct obs_core_hotkeys *hotkeys) hotkeys->platform_context = NULL; } -static xcb_screen_t *default_screen(obs_hotkeys_platform_t *context, - xcb_connection_t *connection) -{ - int def_screen_idx = XDefaultScreen(context->display); - xcb_screen_iterator_t iter; - - iter = xcb_setup_roots_iterator(xcb_get_setup(connection)); - while (iter.rem) { - if (def_screen_idx-- == 0) { - return iter.data; - break; - } - - xcb_screen_next(&iter); - } - - return NULL; -} - -static inline xcb_window_t root_window(obs_hotkeys_platform_t *context, - xcb_connection_t *connection) -{ - xcb_screen_t *screen = default_screen(context, connection); - if (screen) - return screen->root; - return 0; -} - static bool mouse_button_pressed(xcb_connection_t *connection, obs_hotkeys_platform_t *context, obs_key_t key) { + bool ret = false; + +#if USE_XINPUT + memset(context->pressed, 0, XINPUT_MOUSE_LEN); + memset(context->update, 0, XINPUT_MOUSE_LEN); + + xcb_generic_event_t *ev; + while ((ev = xcb_poll_for_event(connection))) { + if ((ev->response_type & ~80) == XCB_GE_GENERIC) { + switch (((xcb_ge_event_t *) ev)->event_type) { + case XCB_INPUT_RAW_BUTTON_PRESS: { + xcb_input_raw_button_press_event_t *mot; + mot = (xcb_input_raw_button_press_event_t *) ev; + if (mot->detail < XINPUT_MOUSE_LEN) { + context->pressed[mot->detail-1] = true; + context->update[mot->detail-1] = true; + } else { + blog(LOG_WARNING, "Unsupported button"); + } + break; + } + case XCB_INPUT_RAW_BUTTON_RELEASE: { + xcb_input_raw_button_release_event_t *mot; + mot = (xcb_input_raw_button_release_event_t *) ev; + if (mot->detail < XINPUT_MOUSE_LEN) + context->update[mot->detail-1] = true; + else + blog(LOG_WARNING, "Unsupported button"); + break; + } + default: + break; + } + } + free(ev); + } + + // Mouse 2 for OBS is Right Click and Mouse 3 is Wheel Click. + // Mouse Wheel axis clicks (xinput mot->detail 4 5 6 7) are ignored. + switch (key) { + case OBS_KEY_MOUSE1: + ret = context->pressed[0] || context->button_pressed[0]; + break; + case OBS_KEY_MOUSE2: + ret = context->pressed[2] || context->button_pressed[2]; + break; + case OBS_KEY_MOUSE3: + ret = context->pressed[1] || context->button_pressed[1]; + break; + case OBS_KEY_MOUSE4: + ret = context->pressed[7] || context->button_pressed[7]; + break; + case OBS_KEY_MOUSE5: + ret = context->pressed[8] || context->button_pressed[8]; + break; + case OBS_KEY_MOUSE6: + ret = context->pressed[9] || context->button_pressed[9]; + break; + case OBS_KEY_MOUSE7: + ret = context->pressed[10] || context->button_pressed[10]; + break; + case OBS_KEY_MOUSE8: + ret = context->pressed[11] || context->button_pressed[11]; + break; + case OBS_KEY_MOUSE9: + ret = context->pressed[12] || context->button_pressed[12]; + break; + case OBS_KEY_MOUSE10: + ret = context->pressed[13] || context->button_pressed[13]; + break; + case OBS_KEY_MOUSE11: + ret = context->pressed[14] || context->button_pressed[14]; + break; + case OBS_KEY_MOUSE12: + ret = context->pressed[15] || context->button_pressed[15]; + break; + case OBS_KEY_MOUSE13: + ret = context->pressed[16] || context->button_pressed[16]; + break; + case OBS_KEY_MOUSE14: + ret = context->pressed[17] || context->button_pressed[17]; + break; + case OBS_KEY_MOUSE15: + ret = context->pressed[18] || context->button_pressed[18]; + break; + case OBS_KEY_MOUSE16: + ret = context->pressed[19] || context->button_pressed[19]; + break; + case OBS_KEY_MOUSE17: + ret = context->pressed[20] || context->button_pressed[20]; + break; + case OBS_KEY_MOUSE18: + ret = context->pressed[21] || context->button_pressed[21]; + break; + case OBS_KEY_MOUSE19: + ret = context->pressed[22] || context->button_pressed[22]; + break; + case OBS_KEY_MOUSE20: + ret = context->pressed[23] || context->button_pressed[23]; + break; + case OBS_KEY_MOUSE21: + ret = context->pressed[24] || context->button_pressed[24]; + break; + case OBS_KEY_MOUSE22: + ret = context->pressed[25] || context->button_pressed[25]; + break; + case OBS_KEY_MOUSE23: + ret = context->pressed[26] || context->button_pressed[26]; + break; + case OBS_KEY_MOUSE24: + ret = context->pressed[27] || context->button_pressed[27]; + break; + case OBS_KEY_MOUSE25: + ret = context->pressed[28] || context->button_pressed[28]; + break; + case OBS_KEY_MOUSE26: + ret = context->pressed[29] || context->button_pressed[29]; + break; + case OBS_KEY_MOUSE27: + ret = context->pressed[30] || context->button_pressed[30]; + break; + case OBS_KEY_MOUSE28: + ret = context->pressed[31] || context->button_pressed[31]; + break; + case OBS_KEY_MOUSE29: + ret = context->pressed[32] || context->button_pressed[32]; + break; + default: + break; + } + + for (int i = 0; i != XINPUT_MOUSE_LEN; i++) + if (context->update[i]) + context->button_pressed[i] = context->pressed[i]; +#else xcb_generic_error_t *error = NULL; xcb_query_pointer_cookie_t qpc; xcb_query_pointer_reply_t *reply; - bool ret = false; qpc = xcb_query_pointer(connection, root_window(context, connection)); reply = xcb_query_pointer_reply(connection, qpc, &error); @@ -789,6 +982,7 @@ static bool mouse_button_pressed(xcb_connection_t *connection, free(reply); free(error); +#endif return ret; } diff --git a/libobs/obs-output.c b/libobs/obs-output.c index 427fa49..f82382f 100644 --- a/libobs/obs-output.c +++ b/libobs/obs-output.c @@ -22,7 +22,7 @@ #if BUILD_CAPTIONS #include -#include +#include #endif static inline bool active(const struct obs_output *output) @@ -419,6 +419,18 @@ bool obs_output_active(const obs_output_t *output) (active(output) || reconnecting(output)) : false; } +uint32_t obs_output_get_flags(const obs_output_t *output) +{ + return obs_output_valid(output, "obs_output_get_flags") ? + output->info.flags : 0; +} + +uint32_t obs_get_output_flags(const char *id) +{ + const struct obs_output_info *info = find_output(id); + return info ? info->flags : 0; +} + static inline obs_data_t *get_defaults(const struct obs_output_info *info) { obs_data_t *settings = obs_data_create(); @@ -977,7 +989,7 @@ static bool add_caption(struct obs_output *output, struct encoder_packet *out) if (out->priority > 1) return false; - sei_init(&sei); + sei_init(&sei, 0.0); da_init(out_data); da_push_back_array(out_data, &ref, sizeof(ref)); @@ -1541,7 +1553,7 @@ static void hook_data_capture(struct obs_output *output, bool encoded, encoded_callback, output); } else { if (has_video) - video_output_connect(output->video, + start_raw_video(output->video, get_video_conversion(output), default_raw_video_callback, output); if (has_audio) @@ -1797,7 +1809,7 @@ static void *end_data_capture_thread(void *data) stop_audio_encoders(output, encoded_callback); } else { if (has_video) - video_output_disconnect(output->video, + stop_raw_video(output->video, default_raw_video_callback, output); if (has_audio) audio_output_disconnect(output->audio, @@ -2082,34 +2094,14 @@ void obs_output_output_caption_text1(obs_output_t *output, const char *text) // split text into 32 character strings int size = (int)strlen(text); - int r; - size_t char_count; - size_t line_length = 0; - size_t trimmed_length = 0; - blog(LOG_DEBUG, "Caption text: %s", text); pthread_mutex_lock(&output->caption_mutex); - for (r = 0 ; 0 < size && CAPTION_LINE_CHARS > r; ++r) { - line_length = utf8_line_length(text); - trimmed_length = utf8_trimmed_length(text, line_length); - char_count = utf8_char_count(text, trimmed_length); - - if (SCREEN_COLS < char_count) { - char_count = utf8_wrap_length(text, CAPTION_LINE_CHARS); - line_length = utf8_string_length(text, char_count + 1); - } - - output->caption_tail = caption_text_new( - text, - line_length, - output->caption_tail, - &output->caption_head); - - text += line_length; - size -= (int)line_length; - } + output->caption_tail = caption_text_new( + text, size, + output->caption_tail, + &output->caption_head); pthread_mutex_unlock(&output->caption_mutex); } diff --git a/libobs/obs-scene.c b/libobs/obs-scene.c index 2b7a7f0..a3ba2d8 100644 --- a/libobs/obs-scene.c +++ b/libobs/obs-scene.c @@ -20,6 +20,21 @@ #include "graphics/math-defs.h" #include "obs-scene.h" +const struct obs_source_info group_info; + +static void resize_group(obs_sceneitem_t *group); +static void resize_scene(obs_scene_t *scene); +static void signal_parent(obs_scene_t *parent, const char *name, + calldata_t *params); +static void get_ungrouped_transform(obs_sceneitem_t *group, + struct vec2 *pos, + struct vec2 *scale, + float *rot); +static inline bool crop_enabled(const struct obs_sceneitem_crop *crop); +static inline bool item_texture_enabled(const struct obs_scene_item *item); +static void init_hotkeys(obs_scene_t *scene, obs_sceneitem_t *item, + const char *name); + /* NOTE: For proper mutex lock order (preventing mutual cross-locks), never * lock the graphics mutex inside either of the scene mutexes. * @@ -48,11 +63,9 @@ static inline void signal_item_remove(struct obs_scene_item *item) uint8_t stack[128]; calldata_init_fixed(¶ms, stack, sizeof(stack)); - calldata_set_ptr(¶ms, "scene", item->parent); calldata_set_ptr(¶ms, "item", item); - signal_handler_signal(item->parent->source->context.signals, - "item_remove", ¶ms); + signal_parent(item->parent, "item_remove", ¶ms); } static const char *scene_getname(void *unused) @@ -61,18 +74,28 @@ static const char *scene_getname(void *unused) return "Scene"; } +static const char *group_getname(void *unused) +{ + UNUSED_PARAMETER(unused); + return "Group"; +} + static void *scene_create(obs_data_t *settings, struct obs_source *source) { pthread_mutexattr_t attr; - struct obs_scene *scene = bmalloc(sizeof(struct obs_scene)); - scene->source = source; - scene->first_item = NULL; + struct obs_scene *scene = bzalloc(sizeof(struct obs_scene)); + scene->source = source; + + if (source->info.id == group_info.id) { + scene->is_group = true; + scene->custom_size = true; + scene->cx = 0; + scene->cy = 0; + } signal_handler_add_array(obs_source_get_signal_handler(source), obs_scene_signals); - scene->id_counter = 0; - if (pthread_mutexattr_init(&attr) != 0) goto fail; if (pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE) != 0) @@ -311,21 +334,29 @@ static inline uint32_t calc_cy(const struct obs_scene_item *item, return (crop_cy > height) ? 2 : (height - crop_cy); } -static void update_item_transform(struct obs_scene_item *item) +static void update_item_transform(struct obs_scene_item *item, bool update_tex) { - uint32_t width = obs_source_get_width(item->source); - uint32_t height = obs_source_get_height(item->source); - uint32_t cx = calc_cx(item, width); - uint32_t cy = calc_cy(item, height); + uint32_t width; + uint32_t height; + uint32_t cx; + uint32_t cy; struct vec2 base_origin; struct vec2 origin; - struct vec2 scale = item->scale; + struct vec2 scale; struct calldata params; uint8_t stack[128]; if (os_atomic_load_long(&item->defer_update) > 0) return; + width = obs_source_get_width(item->source); + height = obs_source_get_height(item->source); + cx = calc_cx(item, width); + cy = calc_cy(item, height); + scale = item->scale; + item->last_width = width; + item->last_height = height; + width = cx; height = cy; @@ -378,14 +409,26 @@ static void update_item_transform(struct obs_scene_item *item) /* ----------------------- */ - item->last_width = width; - item->last_height = height; - calldata_init_fixed(¶ms, stack, sizeof(stack)); - calldata_set_ptr(¶ms, "scene", item->parent); calldata_set_ptr(¶ms, "item", item); - signal_handler_signal(item->parent->source->context.signals, - "item_transform", ¶ms); + signal_parent(item->parent, "item_transform", ¶ms); + + if (!update_tex) + return; + + if (item->item_render && !item_texture_enabled(item)) { + obs_enter_graphics(); + gs_texrender_destroy(item->item_render); + item->item_render = NULL; + obs_leave_graphics(); + + } else if (!item->item_render && item_texture_enabled(item)) { + obs_enter_graphics(); + item->item_render = gs_texrender_create(GS_RGBA, GS_ZS_NONE); + obs_leave_graphics(); + } + + os_atomic_set_bool(&item->update_transform, false); } static inline bool source_size_changed(struct obs_scene_item *item) @@ -414,7 +457,7 @@ static inline bool item_is_scene(const struct obs_scene_item *item) static inline bool item_texture_enabled(const struct obs_scene_item *item) { return crop_enabled(&item->crop) || scale_filter_enabled(item) || - item_is_scene(item); + (item_is_scene(item) && !item->is_group); } static void render_item_texture(struct obs_scene_item *item) @@ -467,6 +510,10 @@ static inline void render_item(struct obs_scene_item *item) if (item->item_render) { uint32_t width = obs_source_get_width(item->source); uint32_t height = obs_source_get_height(item->source); + + if (!width || !height) + return; + uint32_t cx = calc_cx(item, width); uint32_t cy = calc_cy(item, height); @@ -521,6 +568,49 @@ static void scene_video_tick(void *data, float seconds) UNUSED_PARAMETER(seconds); } +/* assumes video lock */ +static void update_transforms_and_prune_sources(obs_scene_t *scene, + struct darray *remove_items, obs_sceneitem_t *group_sceneitem) +{ + struct obs_scene_item *item = scene->first_item; + bool rebuild_group = group_sceneitem && + os_atomic_load_bool(&group_sceneitem->update_group_resize); + + while (item) { + if (obs_source_removed(item->source)) { + struct obs_scene_item *del_item = item; + item = item->next; + + remove_without_release(del_item); + darray_push_back(sizeof(struct obs_scene_item*), + remove_items, &del_item); + rebuild_group = true; + continue; + } + + if (item->is_group) { + obs_scene_t *group_scene = item->source->context.data; + + video_lock(group_scene); + update_transforms_and_prune_sources(group_scene, + remove_items, item); + video_unlock(group_scene); + } + + if (os_atomic_load_bool(&item->update_transform) || + source_size_changed(item)) { + + update_item_transform(item, true); + rebuild_group = true; + } + + item = item->next; + } + + if (rebuild_group && group_sceneitem) + resize_group(group_sceneitem); +} + static void scene_video_render(void *data, gs_effect_t *effect) { DARRAY(struct obs_scene_item*) remove_items; @@ -530,24 +620,17 @@ static void scene_video_render(void *data, gs_effect_t *effect) da_init(remove_items); video_lock(scene); - item = scene->first_item; + + if (!scene->is_group) { + update_transforms_and_prune_sources(scene, &remove_items.da, + NULL); + } gs_blend_state_push(); gs_reset_blend_state(); + item = scene->first_item; while (item) { - if (obs_source_removed(item->source)) { - struct obs_scene_item *del_item = item; - item = item->next; - - remove_without_release(del_item); - da_push_back(remove_items, &del_item); - continue; - } - - if (source_size_changed(item)) - update_item_transform(item); - if (item->user_visible) render_item(item); @@ -586,18 +669,24 @@ static void set_visibility(struct obs_scene_item *item, bool vis) pthread_mutex_unlock(&item->actions_mutex); } +static void scene_load(void *data, obs_data_t *settings); + static void scene_load_item(struct obs_scene *scene, obs_data_t *item_data) { const char *name = obs_data_get_string(item_data, "name"); - obs_source_t *source = obs_get_source_by_name(name); + obs_source_t *source; const char *scale_filter_str; struct obs_scene_item *item; bool visible; bool lock; + if (obs_data_get_bool(item_data, "group_item_backup")) + return; + + source = obs_get_source_by_name(name); if (!source) { - blog(LOG_WARNING, "[scene_load_item] Source %s not found!", - name); + blog(LOG_WARNING, "[scene_load_item] Source %s not " + "found!", name); return; } @@ -611,6 +700,8 @@ static void scene_load_item(struct obs_scene *scene, obs_data_t *item_data) return; } + item->is_group = source->info.id == group_info.id; + obs_data_set_default_int(item_data, "align", OBS_ALIGN_TOP | OBS_ALIGN_LEFT); @@ -673,7 +764,7 @@ static void scene_load_item(struct obs_scene *scene, obs_data_t *item_data) obs_source_release(source); - update_item_transform(item); + update_item_transform(item, false); } static void scene_load(void *data, obs_data_t *settings) @@ -697,22 +788,38 @@ static void scene_load(void *data, obs_data_t *settings) if (obs_data_has_user_value(settings, "id_counter")) scene->id_counter = obs_data_get_int(settings, "id_counter"); + if (obs_data_get_bool(settings, "custom_size")) { + scene->cx = (uint32_t)obs_data_get_int(settings, "cx"); + scene->cy = (uint32_t)obs_data_get_int(settings, "cy"); + scene->custom_size = true; + } + obs_data_array_release(items); } +static void scene_save(void *data, obs_data_t *settings); + static void scene_save_item(obs_data_array_t *array, - struct obs_scene_item *item) + struct obs_scene_item *item, + struct obs_scene_item *backup_group) { obs_data_t *item_data = obs_data_create(); const char *name = obs_source_get_name(item->source); const char *scale_filter; + struct vec2 pos = item->pos; + struct vec2 scale = item->scale; + float rot = item->rot; + + if (backup_group) { + get_ungrouped_transform(backup_group, &pos, &scale, &rot); + } obs_data_set_string(item_data, "name", name); obs_data_set_bool (item_data, "visible", item->user_visible); obs_data_set_bool (item_data, "locked", item->locked); - obs_data_set_double(item_data, "rot", item->rot); - obs_data_set_vec2 (item_data, "pos", &item->pos); - obs_data_set_vec2 (item_data, "scale", &item->scale); + obs_data_set_double(item_data, "rot", rot); + obs_data_set_vec2 (item_data, "pos", &pos); + obs_data_set_vec2 (item_data, "scale", &scale); obs_data_set_int (item_data, "align", (int)item->align); obs_data_set_int (item_data, "bounds_type", (int)item->bounds_type); obs_data_set_int (item_data, "bounds_align", (int)item->bounds_align); @@ -722,6 +829,25 @@ static void scene_save_item(obs_data_array_t *array, obs_data_set_int (item_data, "crop_right", (int)item->crop.right); obs_data_set_int (item_data, "crop_bottom", (int)item->crop.bottom); obs_data_set_int (item_data, "id", item->id); + obs_data_set_bool (item_data, "group_item_backup", !!backup_group); + + if (item->is_group) { + obs_scene_t *group_scene = item->source->context.data; + obs_sceneitem_t *group_item; + + /* save group items as part of main scene, but ignored. + * causes an automatic ungroup if scene collection file + * is loaded in previous versions. */ + full_lock(group_scene); + + group_item = group_scene->first_item; + while (group_item) { + scene_save_item(array, group_item, item); + group_item = group_item->next; + } + + full_unlock(group_scene); + } if (item->scale_filter == OBS_SCALE_POINT) scale_filter = "point"; @@ -753,11 +879,16 @@ static void scene_save(void *data, obs_data_t *settings) item = scene->first_item; while (item) { - scene_save_item(array, item); + scene_save_item(array, item, NULL); item = item->next; } obs_data_set_int(settings, "id_counter", scene->id_counter); + obs_data_set_bool(settings, "custom_size", scene->custom_size); + if (scene->custom_size) { + obs_data_set_int(settings, "cx", scene->cx); + obs_data_set_int(settings, "cy", scene->cy); + } full_unlock(scene); @@ -767,14 +898,14 @@ static void scene_save(void *data, obs_data_t *settings) static uint32_t scene_getwidth(void *data) { - UNUSED_PARAMETER(data); - return obs->video.base_width; + obs_scene_t *scene = data; + return scene->custom_size ? scene->cx : obs->video.base_width; } static uint32_t scene_getheight(void *data) { - UNUSED_PARAMETER(data); - return obs->video.base_height; + obs_scene_t *scene = data; + return scene->custom_size ? scene->cy : obs->video.base_height; } static void apply_scene_item_audio_actions(struct obs_scene_item *item, @@ -1008,20 +1139,51 @@ const struct obs_source_info scene_info = .enum_all_sources = scene_enum_all_sources }; -obs_scene_t *obs_scene_create(const char *name) +const struct obs_source_info group_info = { - struct obs_source *source = obs_source_create("scene", name, NULL, + .id = "group", + .type = OBS_SOURCE_TYPE_SCENE, + .output_flags = OBS_SOURCE_VIDEO | + OBS_SOURCE_CUSTOM_DRAW | + OBS_SOURCE_COMPOSITE, + .get_name = group_getname, + .create = scene_create, + .destroy = scene_destroy, + .video_tick = scene_video_tick, + .video_render = scene_video_render, + .audio_render = scene_audio_render, + .get_width = scene_getwidth, + .get_height = scene_getheight, + .load = scene_load, + .save = scene_save, + .enum_active_sources = scene_enum_active_sources, + .enum_all_sources = scene_enum_all_sources +}; + +static inline obs_scene_t *create_id(const char *id, const char *name) +{ + struct obs_source *source = obs_source_create(id, name, NULL, NULL); return source->context.data; } -obs_scene_t *obs_scene_create_private(const char *name) +static inline obs_scene_t *create_private_id(const char *id, const char *name) { - struct obs_source *source = obs_source_create_private("scene", name, + struct obs_source *source = obs_source_create_private(id, name, NULL); return source->context.data; } +obs_scene_t *obs_scene_create(const char *name) +{ + return create_id("scene", name); +} + +obs_scene_t *obs_scene_create_private(const char *name) +{ + return create_private_id("scene", name); +} + static obs_source_t *get_child_at_idx(obs_scene_t *scene, size_t idx) { struct obs_scene_item *item = scene->first_item; @@ -1062,6 +1224,59 @@ static inline obs_source_t *new_ref(obs_source_t *source) return source; } +static inline void duplicate_item_data(struct obs_scene_item *dst, + struct obs_scene_item *src, bool defer_texture_update, + bool duplicate_hotkeys, bool duplicate_private_data) +{ + struct obs_scene *dst_scene = dst->parent; + + if (!src->user_visible) + set_visibility(dst, false); + + dst->selected = src->selected; + dst->pos = src->pos; + dst->rot = src->rot; + dst->scale = src->scale; + dst->align = src->align; + dst->last_width = src->last_width; + dst->last_height = src->last_height; + dst->output_scale = src->output_scale; + dst->scale_filter = src->scale_filter; + dst->box_transform = src->box_transform; + dst->draw_transform = src->draw_transform; + dst->bounds_type = src->bounds_type; + dst->bounds_align = src->bounds_align; + dst->bounds = src->bounds; + + if (duplicate_hotkeys && !dst_scene->source->context.private) { + obs_data_array_t *data0 = NULL; + obs_data_array_t *data1 = NULL; + + obs_hotkey_pair_save(src->toggle_visibility, &data0, &data1); + obs_hotkey_pair_load(dst->toggle_visibility, data0, data1); + + obs_data_array_release(data0); + obs_data_array_release(data1); + } + + obs_sceneitem_set_crop(dst, &src->crop); + + if (defer_texture_update) { + os_atomic_set_bool(&dst->update_transform, true); + } else { + if (!dst->item_render && item_texture_enabled(dst)) { + obs_enter_graphics(); + dst->item_render = gs_texrender_create( + GS_RGBA, GS_ZS_NONE); + obs_leave_graphics(); + } + } + + if (duplicate_private_data) { + obs_data_apply(dst->private_settings, src->private_settings); + } +} + obs_scene_t *obs_scene_duplicate(obs_scene_t *scene, const char *name, enum obs_scene_duplicate_type type) { @@ -1092,14 +1307,19 @@ obs_scene_t *obs_scene_duplicate(obs_scene_t *scene, const char *name, /* --------------------------------- */ - new_scene = make_private ? - obs_scene_create_private(name) : obs_scene_create(name); + new_scene = make_private + ? create_private_id(scene->source->info.id, name) + : create_id(scene->source->info.id, name); obs_source_copy_filters(new_scene->source, scene->source); obs_data_apply(new_scene->source->private_settings, scene->source->private_settings); + /* never duplicate sub-items for groups */ + if (scene->is_group) + make_unique = false; + for (size_t i = 0; i < items.num; i++) { item = items.array[i]; source = make_unique ? @@ -1115,36 +1335,8 @@ obs_scene_t *obs_scene_duplicate(obs_scene_t *scene, const char *name, continue; } - if (!item->user_visible) - set_visibility(new_item, false); - - new_item->selected = item->selected; - new_item->pos = item->pos; - new_item->rot = item->rot; - new_item->scale = item->scale; - new_item->align = item->align; - new_item->last_width = item->last_width; - new_item->last_height = item->last_height; - new_item->output_scale = item->output_scale; - new_item->scale_filter = item->scale_filter; - new_item->box_transform = item->box_transform; - new_item->draw_transform = item->draw_transform; - new_item->bounds_type = item->bounds_type; - new_item->bounds_align = item->bounds_align; - new_item->bounds = item->bounds; - - new_item->toggle_visibility = - OBS_INVALID_HOTKEY_PAIR_ID; - - obs_sceneitem_set_crop(new_item, &item->crop); - - if (!new_item->item_render && - item_texture_enabled(new_item)) { - obs_enter_graphics(); - new_item->item_render = gs_texrender_create( - GS_RGBA, GS_ZS_NONE); - obs_leave_graphics(); - } + duplicate_item_data(new_item, item, false, false, + false); obs_source_release(source); } @@ -1153,6 +1345,9 @@ obs_scene_t *obs_scene_duplicate(obs_scene_t *scene, const char *name, for (size_t i = 0; i < items.num; i++) obs_sceneitem_release(items.array[i]); + if (new_scene->is_group) + resize_scene(new_scene); + da_free(items); return new_scene; } @@ -1182,6 +1377,14 @@ obs_scene_t *obs_scene_from_source(const obs_source_t *source) return source->context.data; } +obs_scene_t *obs_group_from_source(const obs_source_t *source) +{ + if (!source || source->info.id != group_info.id) + return NULL; + + return source->context.data; +} + obs_sceneitem_t *obs_scene_find_source(obs_scene_t *scene, const char *name) { struct obs_scene_item *item; @@ -1332,18 +1535,55 @@ static void init_hotkeys(obs_scene_t *scene, obs_sceneitem_t *item, dstr_free(&hide_desc); } +static void sceneitem_rename_hotkey(const obs_sceneitem_t *scene_item, + const char *new_name) +{ + struct dstr show = { 0 }; + struct dstr hide = { 0 }; + struct dstr show_desc = { 0 }; + struct dstr hide_desc = { 0 }; + + dstr_copy(&show, "libobs.show_scene_item.%1"); + dstr_replace(&show, "%1", new_name); + dstr_copy(&hide, "libobs.hide_scene_item.%1"); + dstr_replace(&hide, "%1", new_name); + + obs_hotkey_pair_set_names(scene_item->toggle_visibility, + show.array, hide.array); + + dstr_copy(&show_desc, obs->hotkeys.sceneitem_show); + dstr_replace(&show_desc, "%1", new_name); + dstr_copy(&hide_desc, obs->hotkeys.sceneitem_hide); + dstr_replace(&hide_desc, "%1", new_name); + + obs_hotkey_pair_set_descriptions(scene_item->toggle_visibility, + show_desc.array, hide_desc.array); + + dstr_free(&show); + dstr_free(&hide); + dstr_free(&show_desc); + dstr_free(&hide_desc); +} + +static void sceneitem_renamed(void *param, calldata_t *data) +{ + obs_sceneitem_t *scene_item = param; + const char *name = calldata_string(data, "new_name"); + + sceneitem_rename_hotkey(scene_item, name); +} + static inline bool source_has_audio(obs_source_t *source) { return (source->info.output_flags & (OBS_SOURCE_AUDIO | OBS_SOURCE_COMPOSITE)) != 0; } -obs_sceneitem_t *obs_scene_add(obs_scene_t *scene, obs_source_t *source) +static obs_sceneitem_t *obs_scene_add_internal(obs_scene_t *scene, + obs_source_t *source, obs_sceneitem_t *insert_after) { struct obs_scene_item *last; struct obs_scene_item *item; - struct calldata params; - uint8_t stack[128]; pthread_mutex_t mutex; struct item_action action = { @@ -1380,7 +1620,9 @@ obs_sceneitem_t *obs_scene_add(obs_scene_t *scene, obs_source_t *source) item->actions_mutex = mutex; item->user_visible = true; item->locked = false; + item->is_group = source->info.id == group_info.id; item->private_settings = obs_data_create(); + item->toggle_visibility = OBS_INVALID_HOTKEY_PAIR_ID; os_atomic_set_long(&item->active_refs, 1); vec2_set(&item->scale, 1.0f, 1.0f); matrix4_identity(&item->draw_transform); @@ -1403,15 +1645,23 @@ obs_sceneitem_t *obs_scene_add(obs_scene_t *scene, obs_source_t *source) full_lock(scene); - last = scene->first_item; - if (!last) { - scene->first_item = item; + if (insert_after) { + obs_sceneitem_t *next = insert_after->next; + if (next) next->prev = item; + item->next = insert_after->next; + item->prev = insert_after; + insert_after->next = item; } else { - while (last->next) - last = last->next; + last = scene->first_item; + if (!last) { + scene->first_item = item; + } else { + while (last->next) + last = last->next; - last->next = item; - item->prev = last; + last->next = item; + item->prev = last; + } } full_unlock(scene); @@ -1419,12 +1669,23 @@ obs_sceneitem_t *obs_scene_add(obs_scene_t *scene, obs_source_t *source) if (!scene->source->context.private) init_hotkeys(scene, item, obs_source_get_name(source)); + signal_handler_connect(obs_source_get_signal_handler(source), "rename", + sceneitem_renamed, item); + + return item; +} + +obs_sceneitem_t *obs_scene_add(obs_scene_t *scene, obs_source_t *source) +{ + obs_sceneitem_t *item = obs_scene_add_internal(scene, source, NULL); + struct calldata params; + uint8_t stack[128]; + calldata_init_fixed(¶ms, stack, sizeof(stack)); calldata_set_ptr(¶ms, "scene", scene); calldata_set_ptr(¶ms, "item", item); signal_handler_signal(scene->source->context.signals, "item_add", ¶ms); - return item; } @@ -1439,6 +1700,9 @@ static void obs_sceneitem_destroy(obs_sceneitem_t *item) obs_data_release(item->private_settings); obs_hotkey_pair_unregister(item->toggle_visibility); pthread_mutex_destroy(&item->actions_mutex); + signal_handler_disconnect( + obs_source_get_signal_handler(item->source), + "rename", sceneitem_renamed, item); if (item->source) obs_source_release(item->source); da_free(item->audio_actions); @@ -1503,6 +1767,13 @@ obs_source_t *obs_sceneitem_get_source(const obs_sceneitem_t *item) return item ? item->source : NULL; } +static void signal_parent(obs_scene_t *parent, const char *command, + calldata_t *params) +{ + calldata_set_ptr(params, "scene", parent); + signal_handler_signal(parent->source->context.signals, command, params); +} + void obs_sceneitem_select(obs_sceneitem_t *item, bool select) { struct calldata params; @@ -1515,10 +1786,9 @@ void obs_sceneitem_select(obs_sceneitem_t *item, bool select) item->selected = select; calldata_init_fixed(¶ms, stack, sizeof(stack)); - calldata_set_ptr(¶ms, "scene", item->parent); calldata_set_ptr(¶ms, "item", item); - signal_handler_signal(item->parent->source->context.signals, - command, ¶ms); + + signal_parent(item->parent, command, ¶ms); } bool obs_sceneitem_selected(const obs_sceneitem_t *item) @@ -1526,11 +1796,19 @@ bool obs_sceneitem_selected(const obs_sceneitem_t *item) return item ? item->selected : false; } +#define do_update_transform(item) \ + do { \ + if (!item->parent || item->parent->is_group) \ + os_atomic_set_bool(&item->update_transform, true); \ + else \ + update_item_transform(item, false); \ + } while (false) + void obs_sceneitem_set_pos(obs_sceneitem_t *item, const struct vec2 *pos) { if (item) { vec2_copy(&item->pos, pos); - update_item_transform(item); + do_update_transform(item); } } @@ -1538,7 +1816,7 @@ void obs_sceneitem_set_rot(obs_sceneitem_t *item, float rot) { if (item) { item->rot = rot; - update_item_transform(item); + do_update_transform(item); } } @@ -1546,7 +1824,7 @@ void obs_sceneitem_set_scale(obs_sceneitem_t *item, const struct vec2 *scale) { if (item) { vec2_copy(&item->scale, scale); - update_item_transform(item); + do_update_transform(item); } } @@ -1554,7 +1832,7 @@ void obs_sceneitem_set_alignment(obs_sceneitem_t *item, uint32_t alignment) { if (item) { item->align = alignment; - update_item_transform(item); + do_update_transform(item); } } @@ -1567,10 +1845,7 @@ static inline void signal_reorder(struct obs_scene_item *item) command = "reorder"; calldata_init_fixed(¶ms, stack, sizeof(stack)); - calldata_set_ptr(¶ms, "scene", item->parent); - - signal_handler_signal(item->parent->source->context.signals, - command, ¶ms); + signal_parent(item->parent, command, ¶ms); } void obs_sceneitem_set_order(obs_sceneitem_t *item, @@ -1610,9 +1885,9 @@ void obs_sceneitem_set_order(obs_sceneitem_t *item, attach_sceneitem(scene, item, NULL); } - signal_reorder(item); - full_unlock(scene); + + signal_reorder(item); obs_scene_release(scene); } @@ -1642,9 +1917,9 @@ void obs_sceneitem_set_order_position(obs_sceneitem_t *item, attach_sceneitem(scene, item, next); } - signal_reorder(item); - full_unlock(scene); + + signal_reorder(item); obs_scene_release(scene); } @@ -1653,7 +1928,7 @@ void obs_sceneitem_set_bounds_type(obs_sceneitem_t *item, { if (item) { item->bounds_type = type; - update_item_transform(item); + do_update_transform(item); } } @@ -1662,7 +1937,7 @@ void obs_sceneitem_set_bounds_alignment(obs_sceneitem_t *item, { if (item) { item->bounds_align = alignment; - update_item_transform(item); + do_update_transform(item); } } @@ -1670,7 +1945,7 @@ void obs_sceneitem_set_bounds(obs_sceneitem_t *item, const struct vec2 *bounds) { if (item) { item->bounds = *bounds; - update_item_transform(item); + do_update_transform(item); } } @@ -1737,7 +2012,7 @@ void obs_sceneitem_set_info(obs_sceneitem_t *item, item->bounds_type = info->bounds_type; item->bounds_align = info->bounds_alignment; item->bounds = info->bounds; - update_item_transform(item); + do_update_transform(item); } } @@ -1791,12 +2066,10 @@ bool obs_sceneitem_set_visible(obs_sceneitem_t *item, bool visible) item->user_visible = visible; calldata_init_fixed(&cd, stack, sizeof(stack)); - calldata_set_ptr(&cd, "scene", item->parent); calldata_set_ptr(&cd, "item", item); calldata_set_bool(&cd, "visible", visible); - signal_handler_signal(item->parent->source->context.signals, - "item_visible", &cd); + signal_parent(item->parent, "item_visible", &cd); if (source_has_audio(item->source)) { pthread_mutex_lock(&item->actions_mutex); @@ -1888,9 +2161,9 @@ bool obs_scene_reorder_items(obs_scene_t *scene, prev = item_order[i]; } - signal_reorder(scene->first_item); - full_unlock(scene); + + signal_reorder(scene->first_item); obs_scene_release(scene); return true; } @@ -1920,8 +2193,6 @@ static inline bool crop_equal(const struct obs_sceneitem_crop *crop1, void obs_sceneitem_set_crop(obs_sceneitem_t *item, const struct obs_sceneitem_crop *crop) { - bool item_tex_now_enabled; - if (!obs_ptr_valid(item, "obs_sceneitem_set_crop")) return; if (!obs_ptr_valid(crop, "obs_sceneitem_set_crop")) @@ -1929,28 +2200,14 @@ void obs_sceneitem_set_crop(obs_sceneitem_t *item, if (crop_equal(crop, &item->crop)) return; - item_tex_now_enabled = crop_enabled(crop) || - scale_filter_enabled(item) || item_is_scene(item); - - obs_enter_graphics(); - - if (!item_tex_now_enabled) { - gs_texrender_destroy(item->item_render); - item->item_render = NULL; - - } else if (!item->item_render) { - item->item_render = gs_texrender_create(GS_RGBA, GS_ZS_NONE); - } - memcpy(&item->crop, crop, sizeof(*crop)); if (item->crop.left < 0) item->crop.left = 0; if (item->crop.right < 0) item->crop.right = 0; if (item->crop.top < 0) item->crop.top = 0; if (item->crop.bottom < 0) item->crop.bottom = 0; - obs_leave_graphics(); - update_item_transform(item); + os_atomic_set_bool(&item->update_transform, true); } void obs_sceneitem_get_crop(const obs_sceneitem_t *item, @@ -1972,19 +2229,7 @@ void obs_sceneitem_set_scale_filter(obs_sceneitem_t *item, item->scale_filter = filter; - obs_enter_graphics(); - - if (!item_texture_enabled(item)) { - gs_texrender_destroy(item->item_render); - item->item_render = NULL; - - } else if (!item->item_render) { - item->item_render = gs_texrender_create(GS_RGBA, GS_ZS_NONE); - } - - obs_leave_graphics(); - - update_item_transform(item); + os_atomic_set_bool(&item->update_transform, true); } enum obs_scale_type obs_sceneitem_get_scale_filter( @@ -2008,7 +2253,24 @@ void obs_sceneitem_defer_update_end(obs_sceneitem_t *item) return; if (os_atomic_dec_long(&item->defer_update) == 0) - update_item_transform(item); + do_update_transform(item); +} + +void obs_sceneitem_defer_group_resize_begin(obs_sceneitem_t *item) +{ + if (!obs_ptr_valid(item, "obs_sceneitem_defer_group_resize_begin")) + return; + + os_atomic_inc_long(&item->defer_group_resize); +} + +void obs_sceneitem_defer_group_resize_end(obs_sceneitem_t *item) +{ + if (!obs_ptr_valid(item, "obs_sceneitem_defer_group_resize_end")) + return; + + if (os_atomic_dec_long(&item->defer_group_resize) == 0) + os_atomic_set_bool(&item->update_group_resize, true); } int64_t obs_sceneitem_get_id(const obs_sceneitem_t *item) @@ -2027,3 +2289,608 @@ obs_data_t *obs_sceneitem_get_private_settings(obs_sceneitem_t *item) obs_data_addref(item->private_settings); return item->private_settings; } + +static inline void transform_val(struct vec2 *v2, struct matrix4 *transform) +{ + struct vec3 v; + vec3_set(&v, v2->x, v2->y, 0.0f); + vec3_transform(&v, &v, transform); + v2->x = v.x; + v2->y = v.y; +} + +static void get_ungrouped_transform(obs_sceneitem_t *group, + struct vec2 *pos, + struct vec2 *scale, + float *rot) +{ + struct matrix4 transform; + struct matrix4 mat; + struct vec4 x_base; + + vec4_set(&x_base, 1.0f, 0.0f, 0.0f, 0.0f); + + matrix4_copy(&transform, &group->draw_transform); + + transform_val(pos, &transform); + vec4_set(&transform.t, 0.0f, 0.0f, 0.0f, 1.0f); + + vec4_set(&mat.x, scale->x, 0.0f, 0.0f, 0.0f); + vec4_set(&mat.y, 0.0f, scale->y, 0.0f, 0.0f); + vec4_set(&mat.z, 0.0f, 0.0f, 1.0f, 0.0f); + vec4_set(&mat.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_mul(&mat, &mat, &transform); + + scale->x = vec4_len(&mat.x) * (scale->x > 0.0f ? 1.0f : -1.0f); + scale->y = vec4_len(&mat.y) * (scale->y > 0.0f ? 1.0f : -1.0f); + *rot += group->rot; +} + +static void remove_group_transform(obs_sceneitem_t *group, + obs_sceneitem_t *item) +{ + obs_scene_t *parent = item->parent; + if (!parent || !group) + return; + + get_ungrouped_transform(group, &item->pos, &item->scale, &item->rot); + + update_item_transform(item, false); +} + +static void apply_group_transform(obs_sceneitem_t *item, obs_sceneitem_t *group) +{ + struct matrix4 transform; + struct matrix4 mat; + struct vec4 x_base; + + vec4_set(&x_base, 1.0f, 0.0f, 0.0f, 0.0f); + + matrix4_inv(&transform, &group->draw_transform); + + transform_val(&item->pos, &transform); + vec4_set(&transform.t, 0.0f, 0.0f, 0.0f, 1.0f); + + vec4_set(&mat.x, item->scale.x, 0.0f, 0.0f, 0.0f); + vec4_set(&mat.y, 0.0f, item->scale.y, 0.0f, 0.0f); + vec4_set(&mat.z, 0.0f, 0.0f, 1.0f, 0.0f); + vec4_set(&mat.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_mul(&mat, &mat, &transform); + + item->scale.x = vec4_len(&mat.x) * (item->scale.x > 0.0f ? 1.0f : -1.0f); + item->scale.y = vec4_len(&mat.y) * (item->scale.y > 0.0f ? 1.0f : -1.0f); + item->rot -= group->rot; + + update_item_transform(item, false); +} + +static bool resize_scene_base(obs_scene_t *scene, + struct vec2 *minv, + struct vec2 *maxv, + struct vec2 *scale) +{ + vec2_set(minv, M_INFINITE, M_INFINITE); + vec2_set(maxv, -M_INFINITE, -M_INFINITE); + + obs_sceneitem_t *item = scene->first_item; + if (!item) { + scene->cx = 0; + scene->cy = 0; + return false; + } + + while (item) { +#define get_min_max(x_val, y_val) \ + do { \ + struct vec3 v; \ + vec3_set(&v, x_val, y_val, 0.0f); \ + vec3_transform(&v, &v, &item->box_transform); \ + if (v.x < minv->x) minv->x = v.x; \ + if (v.y < minv->y) minv->y = v.y; \ + if (v.x > maxv->x) maxv->x = v.x; \ + if (v.y > maxv->y) maxv->y = v.y; \ + } while (false) + + get_min_max(0.0f, 0.0f); + get_min_max(1.0f, 0.0f); + get_min_max(0.0f, 1.0f); + get_min_max(1.0f, 1.0f); +#undef get_min_max + + item = item->next; + } + + item = scene->first_item; + while (item) { + vec2_sub(&item->pos, &item->pos, minv); + update_item_transform(item, false); + item = item->next; + } + + vec2_sub(scale, maxv, minv); + scene->cx = (uint32_t)ceilf(scale->x); + scene->cy = (uint32_t)ceilf(scale->y); + return true; +} + +static void resize_scene(obs_scene_t *scene) +{ + struct vec2 minv; + struct vec2 maxv; + struct vec2 scale; + resize_scene_base(scene, &minv, &maxv, &scale); +} + +/* assumes group scene and parent scene is locked */ +static void resize_group(obs_sceneitem_t *group) +{ + obs_scene_t *scene = group->source->context.data; + struct vec2 minv; + struct vec2 maxv; + struct vec2 scale; + + if (os_atomic_load_long(&group->defer_group_resize) > 0) + return; + + if (!resize_scene_base(scene, &minv, &maxv, &scale)) + return; + + if (group->bounds_type == OBS_BOUNDS_NONE) { + struct vec2 new_pos; + + if ((group->align & OBS_ALIGN_LEFT) != 0) + new_pos.x = minv.x; + else if ((group->align & OBS_ALIGN_RIGHT) != 0) + new_pos.x = maxv.x; + else + new_pos.x = (maxv.x - minv.x) * 0.5f + minv.x; + + if ((group->align & OBS_ALIGN_TOP) != 0) + new_pos.y = minv.y; + else if ((group->align & OBS_ALIGN_BOTTOM) != 0) + new_pos.y = maxv.y; + else + new_pos.y = (maxv.y - minv.y) * 0.5f + minv.y; + + transform_val(&new_pos, &group->draw_transform); + vec2_copy(&group->pos, &new_pos); + } + + os_atomic_set_bool(&group->update_group_resize, false); + + update_item_transform(group, false); +} + +obs_sceneitem_t *obs_scene_add_group(obs_scene_t *scene, const char *name) +{ + return obs_scene_insert_group(scene, name, NULL, 0); +} + +obs_sceneitem_t *obs_scene_insert_group(obs_scene_t *scene, + const char *name, obs_sceneitem_t **items, size_t count) +{ + if (!scene) + return NULL; + + /* don't allow groups or sub-items of other groups */ + for (size_t i = count; i > 0; i--) { + obs_sceneitem_t *item = items[i - 1]; + if (item->parent != scene || item->is_group) + return NULL; + } + + obs_scene_t *sub_scene = create_id("group", name); + obs_sceneitem_t *last_item = items ? items[count - 1] : NULL; + + obs_sceneitem_t *item = obs_scene_add_internal( + scene, sub_scene->source, last_item); + + obs_scene_release(sub_scene); + + if (!items || !count) + return item; + + /* ------------------------- */ + + full_lock(scene); + full_lock(sub_scene); + sub_scene->first_item = items[0]; + + for (size_t i = count; i > 0; i--) { + size_t idx = i - 1; + remove_group_transform(item, items[idx]); + detach_sceneitem(items[idx]); + } + for (size_t i = 0; i < count; i++) { + size_t idx = i; + if (idx != (count - 1)) { + size_t next_idx = idx + 1; + items[idx]->next = items[next_idx]; + items[next_idx]->prev = items[idx]; + } else { + items[idx]->next = NULL; + } + items[idx]->parent = sub_scene; + apply_group_transform(items[idx], item); + } + items[0]->prev = NULL; + resize_group(item); + full_unlock(sub_scene); + full_unlock(scene); + + /* ------------------------- */ + + return item; +} + +obs_sceneitem_t *obs_scene_get_group(obs_scene_t *scene, const char *name) +{ + if (!scene || !name || !*name) { + return NULL; + } + + obs_sceneitem_t *group = NULL; + obs_sceneitem_t *item; + + full_lock(scene); + + item = scene->first_item; + while (item) { + if (item->is_group && item->source->context.name) { + if (strcmp(item->source->context.name, name) == 0) { + group = item; + break; + } + } + + item = item->next; + } + + full_unlock(scene); + + return group; +} + +bool obs_sceneitem_is_group(obs_sceneitem_t *item) +{ + return item && item->is_group; +} + +obs_scene_t *obs_sceneitem_group_get_scene(const obs_sceneitem_t *item) +{ + return (item && item->is_group) ? item->source->context.data : NULL; +} + +void obs_sceneitem_group_ungroup(obs_sceneitem_t *item) +{ + if (!item || !item->is_group) + return; + + obs_scene_t *scene = item->parent; + obs_scene_t *subscene = item->source->context.data; + obs_sceneitem_t *insert_after = item; + obs_sceneitem_t *first; + obs_sceneitem_t *last; + + full_lock(scene); + + /* ------------------------- */ + + full_lock(subscene); + first = subscene->first_item; + last = first; + while (last) { + obs_sceneitem_t *dst; + + remove_group_transform(item, last); + dst = obs_scene_add_internal(scene, last->source, insert_after); + duplicate_item_data(dst, last, true, true, true); + apply_group_transform(last, item); + + if (!last->next) + break; + + insert_after = dst; + last = last->next; + } + full_unlock(subscene); + + /* ------------------------- */ + + detach_sceneitem(item); + full_unlock(scene); + + obs_sceneitem_release(item); +} + +void obs_sceneitem_group_add_item(obs_sceneitem_t *group, obs_sceneitem_t *item) +{ + if (!group || !group->is_group || !item) + return; + + obs_scene_t *scene = group->parent; + obs_scene_t *groupscene = group->source->context.data; + obs_sceneitem_t *last; + + if (item->parent != scene) + return; + + /* ------------------------- */ + + full_lock(scene); + remove_group_transform(group, item); + detach_sceneitem(item); + + /* ------------------------- */ + + full_lock(groupscene); + last = groupscene->first_item; + if (last) { + for (;;) { + if (!last->next) + break; + last = last->next; + } + last->next = item; + item->prev = last; + } else { + groupscene->first_item = item; + } + item->parent = groupscene; + item->next = NULL; + apply_group_transform(item, group); + resize_group(group); + full_unlock(groupscene); + + /* ------------------------- */ + + full_unlock(scene); +} + +void obs_sceneitem_group_remove_item(obs_sceneitem_t *group, + obs_sceneitem_t *item) +{ + if (!item || !group || !group->is_group) + return; + + obs_scene_t *groupscene = item->parent; + obs_scene_t *scene = group->parent; + + /* ------------------------- */ + + full_lock(scene); + full_lock(groupscene); + remove_group_transform(group, item); + detach_sceneitem(item); + + /* ------------------------- */ + + if (group->prev) { + group->prev->next = item; + item->prev = group->prev; + } else { + scene->first_item = item; + item->prev = NULL; + } + group->prev = item; + item->next = group; + item->parent = scene; + + /* ------------------------- */ + + resize_group(group); + full_unlock(groupscene); + full_unlock(scene); +} + +static void build_current_order_info(obs_scene_t *scene, + struct obs_sceneitem_order_info **items_out, + size_t *size_out) +{ + DARRAY(struct obs_sceneitem_order_info) items; + da_init(items); + + obs_sceneitem_t *item = scene->first_item; + while (item) { + da_push_back(items, &item); + + if (item->is_group) { + obs_scene_t *sub_scene = item->source->context.data; + + full_lock(sub_scene); + + obs_sceneitem_t *sub_item = sub_scene->first_item; + + while (sub_item) { + da_push_back(items, &item); + + sub_item = sub_item->next; + } + + full_unlock(sub_scene); + } + + item = item->next; + } + + *items_out = items.array; + *size_out = items.num; +} + +static bool sceneitems_match2(obs_scene_t *scene, + struct obs_sceneitem_order_info *items, size_t size) +{ + struct obs_sceneitem_order_info *cur_items; + size_t cur_size; + + build_current_order_info(scene, &cur_items, &cur_size); + if (cur_size != size) { + bfree(cur_items); + return false; + } + + for (size_t i = 0; i < size; i++) { + struct obs_sceneitem_order_info *new = &items[i]; + struct obs_sceneitem_order_info *old = &cur_items[i]; + + if (new->group != old->group || new->item != old->item) { + bfree(cur_items); + return false; + } + } + + bfree(cur_items); + return true; +} + +static obs_sceneitem_t *get_sceneitem_parent_group(obs_scene_t *scene, + obs_sceneitem_t *group_subitem) +{ + if (group_subitem->is_group) + return NULL; + + obs_sceneitem_t *item = scene->first_item; + while (item) { + if (item->is_group && + item->source->context.data == group_subitem->parent) + return item; + item = item->next; + } + + return NULL; +} + +bool obs_scene_reorder_items2(obs_scene_t *scene, + struct obs_sceneitem_order_info *item_order, + size_t item_order_size) +{ + if (!scene || !item_order_size || !item_order) + return false; + + obs_scene_addref(scene); + full_lock(scene); + + if (sceneitems_match2(scene, item_order, item_order_size)) { + full_unlock(scene); + obs_scene_release(scene); + return false; + } + + for (size_t i = 0; i < item_order_size; i++) { + struct obs_sceneitem_order_info *info = &item_order[i]; + if (!info->item->is_group) { + obs_sceneitem_t *group = + get_sceneitem_parent_group(scene, info->item); + remove_group_transform(group, info->item); + } + } + + scene->first_item = item_order[0].item; + + obs_sceneitem_t *prev = NULL; + for (size_t i = 0; i < item_order_size; i++) { + struct obs_sceneitem_order_info *info = &item_order[i]; + obs_sceneitem_t *item = info->item; + + if (info->item->is_group) { + obs_sceneitem_t *sub_prev = NULL; + obs_scene_t *sub_scene = + info->item->source->context.data; + + sub_scene->first_item = NULL; + + obs_scene_addref(sub_scene); + full_lock(sub_scene); + + for (i++; i < item_order_size; i++) { + struct obs_sceneitem_order_info *sub_info = + &item_order[i]; + obs_sceneitem_t *sub_item = sub_info->item; + + if (sub_info->group != info->item) { + i--; + break; + } + + if (!sub_scene->first_item) + sub_scene->first_item = sub_item; + + sub_item->prev = sub_prev; + sub_item->next = NULL; + sub_item->parent = sub_scene; + + if (sub_prev) + sub_prev->next = sub_item; + + apply_group_transform(sub_info->item, + sub_info->group); + + sub_prev = sub_item; + } + + resize_group(info->item); + full_unlock(sub_scene); + obs_scene_release(sub_scene); + } + + item->prev = prev; + item->next = NULL; + item->parent = scene; + + if (prev) + prev->next = item; + + prev = item; + } + + full_unlock(scene); + + signal_reorder(scene->first_item); + obs_scene_release(scene); + return true; +} + +obs_sceneitem_t *obs_sceneitem_get_group(obs_scene_t *scene, + obs_sceneitem_t *group_subitem) +{ + if (!scene || !group_subitem || group_subitem->is_group) + return NULL; + + full_lock(scene); + obs_sceneitem_t *group = get_sceneitem_parent_group(scene, + group_subitem); + full_unlock(scene); + + return group; +} + +bool obs_source_is_group(const obs_source_t *source) +{ + return source && source->info.id == group_info.id; +} + +bool obs_scene_is_group(const obs_scene_t *scene) +{ + return scene ? scene->is_group : false; +} + +void obs_sceneitem_group_enum_items(obs_sceneitem_t *group, + bool (*callback)(obs_scene_t*, obs_sceneitem_t*, void*), + void *param) +{ + if (!group || !group->is_group) + return; + + obs_scene_t *scene = group->source->context.data; + if (scene) + obs_scene_enum_items(scene, callback, param); +} + +void obs_sceneitem_force_update_transform(obs_sceneitem_t *item) +{ + if (!item) + return; + + if (os_atomic_set_bool(&item->update_transform, false)) + update_item_transform(item, false); +} diff --git a/libobs/obs-scene.h b/libobs/obs-scene.h index 06db0a6..4591db5 100644 --- a/libobs/obs-scene.h +++ b/libobs/obs-scene.h @@ -32,12 +32,17 @@ struct obs_scene_item { volatile long ref; volatile bool removed; + bool is_group; + bool update_transform; + bool update_group_resize; + int64_t id; struct obs_scene *parent; struct obs_source *source; volatile long active_refs; volatile long defer_update; + volatile long defer_group_resize; bool user_visible; bool visible; bool selected; @@ -81,6 +86,11 @@ struct obs_scene_item { struct obs_scene { struct obs_source *source; + bool is_group; + bool custom_size; + uint32_t cx; + uint32_t cy; + int64_t id_counter; pthread_mutex_t video_mutex; diff --git a/libobs/obs-source.c b/libobs/obs-source.c index 8697117..d77be32 100644 --- a/libobs/obs-source.c +++ b/libobs/obs-source.c @@ -347,10 +347,14 @@ static obs_source_t *obs_source_create_internal(const char *id, blog(LOG_DEBUG, "%ssource '%s' (%s) created", private ? "private " : "", name, id); - obs_source_dosignal(source, "source_create", NULL); source->flags = source->default_flags; source->enabled = true; + + if (!private) { + obs_source_dosignal(source, "source_create", NULL); + } + return source; fail: @@ -427,9 +431,16 @@ static void duplicate_filters(obs_source_t *dst, obs_source_t *src, void obs_source_copy_filters(obs_source_t *dst, obs_source_t *src) { + if (!obs_source_valid(dst, "obs_source_copy_filters")) + return; + if (!obs_source_valid(src, "obs_source_copy_filters")) + return; + duplicate_filters(dst, src, dst->context.private); } +extern obs_scene_t *obs_group_from_source(const obs_source_t *source); + obs_source_t *obs_source_duplicate(obs_source_t *source, const char *new_name, bool create_private) { @@ -446,6 +457,11 @@ obs_source_t *obs_source_duplicate(obs_source_t *source, if (source->info.type == OBS_SOURCE_TYPE_SCENE) { obs_scene_t *scene = obs_scene_from_source(source); + if (!scene) + scene = obs_group_from_source(source); + if (!scene) + return NULL; + obs_scene_t *new_scene = obs_scene_duplicate(scene, new_name, create_private ? OBS_SCENE_DUP_PRIVATE_COPY : OBS_SCENE_DUP_COPY); @@ -1720,9 +1736,18 @@ static inline void obs_source_render_async_video(obs_source_t *source) static inline void obs_source_render_filters(obs_source_t *source) { + obs_source_t *first_filter; + + pthread_mutex_lock(&source->filter_mutex); + first_filter = source->filters.array[0]; + obs_source_addref(first_filter); + pthread_mutex_unlock(&source->filter_mutex); + source->rendering_filter = true; - obs_source_video_render(source->filters.array[0]); + obs_source_video_render(first_filter); source->rendering_filter = false; + + obs_source_release(first_filter); } void obs_source_default_render(obs_source_t *source) @@ -2270,6 +2295,12 @@ static void copy_frame_data(struct obs_source_frame *dst, } } +void obs_source_frame_copy(struct obs_source_frame *dst, + const struct obs_source_frame *src) +{ + copy_frame_data(dst, src); +} + static inline bool async_texture_changed(struct obs_source *source, const struct obs_source_frame *frame) { diff --git a/libobs/obs-video.c b/libobs/obs-video.c index d7641ad..aaae79a 100644 --- a/libobs/obs-video.c +++ b/libobs/obs-video.c @@ -334,8 +334,8 @@ end: profile_end(stage_output_texture_name); } -static inline void render_video(struct obs_core_video *video, int cur_texture, - int prev_texture) +static inline void render_video(struct obs_core_video *video, bool raw_active, + int cur_texture, int prev_texture) { gs_begin_scene(); @@ -343,11 +343,14 @@ static inline void render_video(struct obs_core_video *video, int cur_texture, gs_set_cull_mode(GS_NEITHER); render_main_texture(video, cur_texture); - render_output_texture(video, cur_texture, prev_texture); - if (video->gpu_conversion) - render_convert_texture(video, cur_texture, prev_texture); - stage_output_texture(video, cur_texture, prev_texture); + if (raw_active) { + render_output_texture(video, cur_texture, prev_texture); + if (video->gpu_conversion) + render_convert_texture(video, cur_texture, prev_texture); + + stage_output_texture(video, cur_texture, prev_texture); + } gs_set_render_target(NULL, NULL); gs_enable_blending(true); @@ -522,7 +525,7 @@ static inline void output_video_data(struct obs_core_video *video, } } -static inline void video_sleep(struct obs_core_video *video, +static inline void video_sleep(struct obs_core_video *video, bool active, uint64_t *p_time, uint64_t interval_ns) { struct obs_vframe_info vframe_info; @@ -543,8 +546,9 @@ static inline void video_sleep(struct obs_core_video *video, vframe_info.timestamp = cur_time; vframe_info.count = count; - circlebuf_push_back(&video->vframe_info_buffer, &vframe_info, - sizeof(vframe_info)); + if (active) + circlebuf_push_back(&video->vframe_info_buffer, &vframe_info, + sizeof(vframe_info)); } static const char *output_frame_gs_context_name = "gs_context(video->graphics)"; @@ -552,7 +556,7 @@ static const char *output_frame_render_video_name = "render_video"; static const char *output_frame_download_frame_name = "download_frame"; static const char *output_frame_gs_flush_name = "gs_flush"; static const char *output_frame_output_video_data_name = "output_video_data"; -static inline void output_frame(void) +static inline void output_frame(bool raw_active) { struct obs_core_video *video = &obs->video; int cur_texture = video->cur_texture; @@ -566,12 +570,14 @@ static inline void output_frame(void) gs_enter_context(video->graphics); profile_start(output_frame_render_video_name); - render_video(video, cur_texture, prev_texture); + render_video(video, raw_active, cur_texture, prev_texture); profile_end(output_frame_render_video_name); - profile_start(output_frame_download_frame_name); - frame_ready = download_frame(video, prev_texture, &frame); - profile_end(output_frame_download_frame_name); + if (raw_active) { + profile_start(output_frame_download_frame_name); + frame_ready = download_frame(video, prev_texture, &frame); + profile_end(output_frame_download_frame_name); + } profile_start(output_frame_gs_flush_name); gs_flush(); @@ -580,7 +586,7 @@ static inline void output_frame(void) gs_leave_context(); profile_end(output_frame_gs_context_name); - if (frame_ready) { + if (raw_active && frame_ready) { struct obs_vframe_info vframe_info; circlebuf_pop_front(&video->vframe_info_buffer, &vframe_info, sizeof(vframe_info)); @@ -597,6 +603,17 @@ static inline void output_frame(void) #define NBSP "\xC2\xA0" +static void clear_frame_data(void) +{ + struct obs_core_video *video = &obs->video; + memset(video->textures_rendered, 0, sizeof(video->textures_rendered)); + memset(video->textures_output, 0, sizeof(video->textures_output)); + memset(video->textures_copied, 0, sizeof(video->textures_copied)); + memset(video->textures_converted, 0, sizeof(video->textures_converted)); + circlebuf_free(&video->vframe_info_buffer); + video->cur_texture = 0; +} + static const char *tick_sources_name = "tick_sources"; static const char *render_displays_name = "render_displays"; static const char *output_frame_name = "output_frame"; @@ -607,6 +624,7 @@ void *obs_graphics_thread(void *param) uint64_t frame_time_total_ns = 0; uint64_t fps_total_ns = 0; uint32_t fps_total_frames = 0; + bool raw_was_active = false; obs->video.video_time = os_gettime_ns(); @@ -622,6 +640,11 @@ void *obs_graphics_thread(void *param) while (!video_output_stopped(obs->video.video)) { uint64_t frame_start = os_gettime_ns(); uint64_t frame_time_ns; + bool raw_active = obs->video.raw_active > 0; + + if (!raw_was_active && raw_active) + clear_frame_data(); + raw_was_active = raw_active; profile_start(video_thread_name); @@ -630,7 +653,7 @@ void *obs_graphics_thread(void *param) profile_end(tick_sources_name); profile_start(output_frame_name); - output_frame(); + output_frame(raw_active); profile_end(output_frame_name); profile_start(render_displays_name); @@ -643,7 +666,8 @@ void *obs_graphics_thread(void *param) profile_reenable_thread(); - video_sleep(&obs->video, &obs->video.video_time, interval); + video_sleep(&obs->video, raw_active, &obs->video.video_time, + interval); frame_time_total_ns += frame_time_ns; fps_total_ns += (obs->video.video_time - last_time); diff --git a/libobs/obs-win-crash-handler.c b/libobs/obs-win-crash-handler.c index 41987dd..33ffff9 100644 --- a/libobs/obs-win-crash-handler.c +++ b/libobs/obs-win-crash-handler.c @@ -255,10 +255,16 @@ static inline void write_header(struct exception_handler_data *data) ts = *localtime(&now); strftime(date_time, sizeof(date_time), "%Y-%m-%d, %X", &ts); + const char *obs_bitness; + if (sizeof(void*) == 8) + obs_bitness = "64"; + else + obs_bitness = "32"; + dstr_catf(&data->str, "Unhandled exception: %x\r\n" "Date/Time: %s\r\n" "Fault address: %"PRIX64" (%s)\r\n" - "libobs version: "OBS_VERSION"\r\n" + "libobs version: "OBS_VERSION" (%s-bit)\r\n" "Windows version: %d.%d build %d (revision: %d; " "%s-bit)\r\n" "CPU: %s\r\n\r\n", @@ -266,6 +272,7 @@ static inline void write_header(struct exception_handler_data *data) date_time, data->main_trace.instruction_ptr, data->module_name.array, + obs_bitness, data->win_version.major, data->win_version.minor, data->win_version.build, data->win_version.revis, is_64_bit_windows() ? "64" : "32", diff --git a/libobs/obs-windows.c b/libobs/obs-windows.c index 75a9278..e6bbcb0 100644 --- a/libobs/obs-windows.c +++ b/libobs/obs-windows.c @@ -788,6 +788,13 @@ void reset_win32_symbol_paths(void) da_free(paths); } +extern void initialize_crash_handler(void); + +void obs_init_win32_crash_handler(void) +{ + initialize_crash_handler(); +} + void initialize_com(void) { CoInitializeEx(0, COINIT_MULTITHREADED); diff --git a/libobs/obs.c b/libobs/obs.c index f37d854..acd51bb 100644 --- a/libobs/obs.c +++ b/libobs/obs.c @@ -218,7 +218,7 @@ static bool obs_init_textures(struct obs_video_info *ovi) gs_effect_t *obs_load_effect(gs_effect_t **effect, const char *file) { if (!*effect) { - char *filename = find_libobs_data_file(file); + char *filename = obs_find_data_file(file); *effect = gs_effect_create_from_file(filename, NULL); bfree(filename); } @@ -250,49 +250,49 @@ static int obs_init_graphics(struct obs_video_info *ovi) gs_enter_context(video->graphics); - char *filename = find_libobs_data_file("default.effect"); + char *filename = obs_find_data_file("default.effect"); video->default_effect = gs_effect_create_from_file(filename, NULL); bfree(filename); if (gs_get_device_type() == GS_DEVICE_OPENGL) { - filename = find_libobs_data_file("default_rect.effect"); + filename = obs_find_data_file("default_rect.effect"); video->default_rect_effect = gs_effect_create_from_file( filename, NULL); bfree(filename); } - filename = find_libobs_data_file("opaque.effect"); + filename = obs_find_data_file("opaque.effect"); video->opaque_effect = gs_effect_create_from_file(filename, NULL); bfree(filename); - filename = find_libobs_data_file("solid.effect"); + filename = obs_find_data_file("solid.effect"); video->solid_effect = gs_effect_create_from_file(filename, NULL); bfree(filename); - filename = find_libobs_data_file("format_conversion.effect"); + filename = obs_find_data_file("format_conversion.effect"); video->conversion_effect = gs_effect_create_from_file(filename, NULL); bfree(filename); - filename = find_libobs_data_file("bicubic_scale.effect"); + filename = obs_find_data_file("bicubic_scale.effect"); video->bicubic_effect = gs_effect_create_from_file(filename, NULL); bfree(filename); - filename = find_libobs_data_file("lanczos_scale.effect"); + filename = obs_find_data_file("lanczos_scale.effect"); video->lanczos_effect = gs_effect_create_from_file(filename, NULL); bfree(filename); - filename = find_libobs_data_file("bilinear_lowres_scale.effect"); + filename = obs_find_data_file("bilinear_lowres_scale.effect"); video->bilinear_lowres_effect = gs_effect_create_from_file(filename, NULL); bfree(filename); - filename = find_libobs_data_file("premultiplied_alpha.effect"); + filename = obs_find_data_file("premultiplied_alpha.effect"); video->premultiplied_alpha_effect = gs_effect_create_from_file(filename, NULL); bfree(filename); @@ -565,6 +565,7 @@ static bool obs_init_data(void) if (!obs_view_init(&data->main_view)) goto fail; + data->private_data = obs_data_create(); data->valid = true; fail: @@ -620,6 +621,7 @@ static void obs_free_data(void) pthread_mutex_destroy(&data->draw_callbacks_mutex); da_free(data->draw_callbacks); da_free(data->tick_callbacks); + obs_data_release(data->private_data); } static const char *obs_signals[] = { @@ -741,6 +743,7 @@ static inline void obs_free_hotkeys(void) } extern const struct obs_source_info scene_info; +extern const struct obs_source_info group_info; extern void log_system_info(void); @@ -771,16 +774,60 @@ static bool obs_init(const char *locale, const char *module_config_path, obs->module_config_path = bstrdup(module_config_path); obs->locale = bstrdup(locale); obs_register_source(&scene_info); + obs_register_source(&group_info); add_default_module_paths(); return true; } #ifdef _WIN32 -extern void initialize_crash_handler(void); extern void initialize_com(void); extern void uninitialize_com(void); #endif +/* Separate from actual context initialization + * since this can be set before startup and persist + * after shutdown. */ +static DARRAY(struct dstr) core_module_paths = {0}; + +char *obs_find_data_file(const char *file) +{ + struct dstr path = {0}; + + char *result = find_libobs_data_file(file); + if (result) + return result; + + for (size_t i = 0; i < core_module_paths.num; ++i) { + if (check_path(file, core_module_paths.array[i].array, &path)) + return path.array; + } + + dstr_free(&path); + return NULL; +} + +void obs_add_data_path(const char *path) +{ + struct dstr *new_path = da_push_back_new(core_module_paths); + dstr_init_copy(new_path, path); + da_push_back(core_module_paths, new_path); +} + +bool obs_remove_data_path(const char *path) +{ + for (size_t i = 0; i < core_module_paths.num; ++i) { + int result = dstr_cmp(&core_module_paths.array[i], path); + + if (result == 0) { + dstr_free(&core_module_paths.array[i]); + da_erase(core_module_paths, i); + return true; + } + } + + return false; +} + static const char *obs_startup_name = "obs_startup"; bool obs_startup(const char *locale, const char *module_config_path, profiler_name_store_t *store) @@ -795,7 +842,6 @@ bool obs_startup(const char *locale, const char *module_config_path, } #ifdef _WIN32 - initialize_crash_handler(); initialize_com(); #endif @@ -1262,10 +1308,14 @@ void obs_enum_sources(bool (*enum_proc)(void*, obs_source_t*), void *param) obs_source_t *next_source = (obs_source_t*)source->context.next; - if ((source->info.type == OBS_SOURCE_TYPE_INPUT) != 0 && - !source->context.private && - !enum_proc(param, source)) + if (source->info.id == group_info.id && + !enum_proc(param, source)) { break; + } else if (source->info.type == OBS_SOURCE_TYPE_INPUT && + !source->context.private && + !enum_proc(param, source)) { + break; + } source = next_source; } @@ -1468,6 +1518,23 @@ void obs_render_main_texture(void) gs_draw_sprite(tex, 0, 0, 0); } +gs_texture_t *obs_get_main_texture(void) +{ + struct obs_core_video *video = &obs->video; + int last_tex; + + if (!obs) return NULL; + + last_tex = video->cur_texture == 0 + ? NUM_TEXTURES - 1 + : video->cur_texture - 1; + + if (!video->textures_rendered[last_tex]) + return NULL; + + return video->render_textures[last_tex]; +} + void obs_set_master_volume(float volume) { struct calldata data = {0}; @@ -1627,7 +1694,8 @@ void obs_load_sources(obs_data_array_t *array, obs_load_source_cb cb, if (source->info.type == OBS_SOURCE_TYPE_TRANSITION) obs_transition_load(source, source_data); obs_source_load(source); - cb(private_data, source); + if (cb) + cb(private_data, source); } obs_data_release(source_data); } @@ -1950,7 +2018,7 @@ bool obs_set_audio_monitoring_device(const char *name, const char *id) if (!obs || !name || !id || !*name || !*id) return false; -#if defined(_WIN32) || HAVE_PULSEAUDIO +#if defined(_WIN32) || HAVE_PULSEAUDIO || defined(__APPLE__) pthread_mutex_lock(&obs->audio.monitoring_mutex); if (strcmp(id, obs->audio.monitoring_device_id) == 0) { @@ -2054,3 +2122,70 @@ uint32_t obs_get_lagged_frames(void) { return obs ? obs->video.lagged_frames : 0; } + +void start_raw_video(video_t *v, const struct video_scale_info *conversion, + void (*callback)(void *param, struct video_data *frame), + void *param) +{ + struct obs_core_video *video = &obs->video; + os_atomic_inc_long(&video->raw_active); + video_output_connect(v, conversion, callback, param); +} + +void stop_raw_video(video_t *v, + void (*callback)(void *param, struct video_data *frame), + void *param) +{ + struct obs_core_video *video = &obs->video; + os_atomic_dec_long(&video->raw_active); + video_output_disconnect(v, callback, param); +} + +void obs_add_raw_video_callback( + const struct video_scale_info *conversion, + void (*callback)(void *param, struct video_data *frame), + void *param) +{ + struct obs_core_video *video = &obs->video; + if (!obs) + return; + start_raw_video(video->video, conversion, callback, param); +} + +void obs_remove_raw_video_callback( + void (*callback)(void *param, struct video_data *frame), + void *param) +{ + struct obs_core_video *video = &obs->video; + if (!obs) + return; + stop_raw_video(video->video, callback, param); +} + +void obs_apply_private_data(obs_data_t *settings) +{ + if (!obs || !settings) + return; + + obs_data_apply(obs->data.private_data, settings); +} + +void obs_set_private_data(obs_data_t *settings) +{ + if (!obs) + return; + + obs_data_clear(obs->data.private_data); + if (settings) + obs_data_apply(obs->data.private_data, settings); +} + +obs_data_t *obs_get_private_data(void) +{ + if (!obs) + return NULL; + + obs_data_t *private_data = obs->data.private_data; + obs_data_addref(private_data); + return private_data; +} diff --git a/libobs/obs.h b/libobs/obs.h index 6778246..181bd6f 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -242,6 +242,32 @@ struct obs_source_frame { /* ------------------------------------------------------------------------- */ /* OBS context */ +/** + * Find a core libobs data file + * @param path name of the base file + * @return A string containing the full path to the file. + * Use bfree after use. + */ +EXPORT char *obs_find_data_file(const char *file); + +/** + * Add a path to search libobs data files in. + * @param path Full path to directory to look in. + * The string is copied. + */ +EXPORT void obs_add_data_path(const char *path); + +/** + * Remove a path from libobs core data paths. + * @param path The path to compare to currently set paths. + * It does not need to be the same pointer, but + * the path string must match an entry fully. + * @return Whether or not the path was successfully removed. + * If false, the path could not be found. + */ +EXPORT bool obs_remove_data_path(const char *path); + + /** * Initializes OBS * @@ -276,6 +302,12 @@ EXPORT void obs_set_locale(const char *locale); /** @return the current locale */ EXPORT const char *obs_get_locale(void); +/** Initialize the Windows-specific crash handler */ + +#ifdef _WIN32 +EXPORT void obs_init_win32_crash_handler(void); +#endif + /** * Returns the profiler name store (see util/profiler.h) used by OBS, which is * either a name store passed to obs_startup, an internal name store, or NULL @@ -564,6 +596,10 @@ EXPORT void obs_render_main_view(void); /** Renders the last main output texture */ EXPORT void obs_render_main_texture(void); +/** Returns the last main output texture. This can return NULL if the texture + * is unavailable. */ +EXPORT gs_texture_t *obs_get_main_texture(void); + /** Sets the master user volume */ EXPORT void obs_set_master_volume(float volume); @@ -576,6 +612,12 @@ EXPORT obs_data_t *obs_save_source(obs_source_t *source); /** Loads a source from settings data */ EXPORT obs_source_t *obs_load_source(obs_data_t *data); +/** Send a save signal to sources */ +EXPORT void obs_source_save(obs_source_t *source); + +/** Send a load signal to sources */ +EXPORT void obs_source_load(obs_source_t *source); + typedef void (*obs_load_source_cb)(void *private_data, obs_source_t *source); /** Loads sources from a data array */ @@ -624,6 +666,26 @@ EXPORT void obs_remove_main_render_callback( void (*draw)(void *param, uint32_t cx, uint32_t cy), void *param); +EXPORT void obs_add_raw_video_callback( + const struct video_scale_info *conversion, + void (*callback)(void *param, struct video_data *frame), + void *param); +EXPORT void obs_remove_raw_video_callback( + void (*callback)(void *param, struct video_data *frame), + void *param); + +EXPORT uint64_t obs_get_video_frame_time(void); + +EXPORT double obs_get_active_fps(void); +EXPORT uint64_t obs_get_average_frame_time_ns(void); + +EXPORT uint32_t obs_get_total_frames(void); +EXPORT uint32_t obs_get_lagged_frames(void); + +EXPORT void obs_apply_private_data(obs_data_t *settings); +EXPORT void obs_set_private_data(obs_data_t *settings); +EXPORT obs_data_t *obs_get_private_data(void); + /* ------------------------------------------------------------------------- */ /* View context */ @@ -650,14 +712,6 @@ EXPORT obs_source_t *obs_view_get_source(obs_view_t *view, /** Renders the sources of this view context */ EXPORT void obs_view_render(obs_view_t *view); -EXPORT uint64_t obs_get_video_frame_time(void); - -EXPORT double obs_get_active_fps(void); -EXPORT uint64_t obs_get_average_frame_time_ns(void); - -EXPORT uint32_t obs_get_total_frames(void); -EXPORT uint32_t obs_get_lagged_frames(void); - /* ------------------------------------------------------------------------- */ /* Display context */ @@ -1275,6 +1329,15 @@ EXPORT void obs_scene_enum_items(obs_scene_t *scene, EXPORT bool obs_scene_reorder_items(obs_scene_t *scene, obs_sceneitem_t * const *item_order, size_t item_order_size); +struct obs_sceneitem_order_info { + obs_sceneitem_t *group; + obs_sceneitem_t *item; +}; + +EXPORT bool obs_scene_reorder_items2(obs_scene_t *scene, + struct obs_sceneitem_order_info *item_order, + size_t item_order_size); + /** Adds/creates a new scene item for a source */ EXPORT obs_sceneitem_t *obs_scene_add(obs_scene_t *scene, obs_source_t *source); @@ -1364,6 +1427,8 @@ EXPORT void obs_sceneitem_set_scale_filter(obs_sceneitem_t *item, EXPORT enum obs_scale_type obs_sceneitem_get_scale_filter( obs_sceneitem_t *item); +EXPORT void obs_sceneitem_force_update_transform(obs_sceneitem_t *item); + EXPORT void obs_sceneitem_defer_update_begin(obs_sceneitem_t *item); EXPORT void obs_sceneitem_defer_update_end(obs_sceneitem_t *item); @@ -1371,6 +1436,39 @@ EXPORT void obs_sceneitem_defer_update_end(obs_sceneitem_t *item); * automatically. Returns an incremented reference. */ EXPORT obs_data_t *obs_sceneitem_get_private_settings(obs_sceneitem_t *item); +EXPORT obs_sceneitem_t *obs_scene_add_group(obs_scene_t *scene, + const char *name); +EXPORT obs_sceneitem_t *obs_scene_insert_group(obs_scene_t *scene, + const char *name, obs_sceneitem_t **items, size_t count); + +EXPORT obs_sceneitem_t *obs_scene_get_group(obs_scene_t *scene, + const char *name); + +EXPORT bool obs_sceneitem_is_group(obs_sceneitem_t *item); + +EXPORT obs_scene_t *obs_sceneitem_group_get_scene( + const obs_sceneitem_t *group); + +EXPORT void obs_sceneitem_group_ungroup(obs_sceneitem_t *group); + +EXPORT void obs_sceneitem_group_add_item(obs_sceneitem_t *group, + obs_sceneitem_t *item); +EXPORT void obs_sceneitem_group_remove_item(obs_sceneitem_t *group, + obs_sceneitem_t *item); + +EXPORT obs_sceneitem_t *obs_sceneitem_get_group(obs_scene_t *scene, + obs_sceneitem_t *item); + +EXPORT bool obs_source_is_group(const obs_source_t *source); +EXPORT bool obs_scene_is_group(const obs_scene_t *scene); + +EXPORT void obs_sceneitem_group_enum_items(obs_sceneitem_t *group, + bool (*callback)(obs_scene_t*, obs_sceneitem_t*, void*), + void *param); + +EXPORT void obs_sceneitem_defer_group_resize_begin(obs_sceneitem_t *item); +EXPORT void obs_sceneitem_defer_group_resize_end(obs_sceneitem_t *item); + /* ------------------------------------------------------------------------- */ /* Outputs */ @@ -1440,6 +1538,12 @@ EXPORT void obs_output_force_stop(obs_output_t *output); /** Returns whether the output is active */ EXPORT bool obs_output_active(const obs_output_t *output); +/** Returns output capability flags */ +EXPORT uint32_t obs_output_get_flags(const obs_output_t *output); + +/** Returns output capability flags */ +EXPORT uint32_t obs_get_output_flags(const char *id); + /** Gets the default settings for an output type */ EXPORT obs_data_t *obs_output_defaults(const char *id); @@ -1717,6 +1821,7 @@ EXPORT enum video_format obs_encoder_get_preferred_video_format( /** Gets the default settings for an encoder type */ EXPORT obs_data_t *obs_encoder_defaults(const char *id); +EXPORT obs_data_t *obs_encoder_get_defaults(const obs_encoder_t *encoder); /** Returns the property list, if any. Free with obs_properties_destroy */ EXPORT obs_properties_t *obs_get_encoder_properties(const char *id); @@ -1895,6 +2000,8 @@ static inline void obs_source_frame_destroy(struct obs_source_frame *frame) } } +EXPORT void obs_source_frame_copy(struct obs_source_frame *dst, + const struct obs_source_frame *src); #ifdef __cplusplus } diff --git a/libobs/obs.hpp b/libobs/obs.hpp index 11fbe29..78780fc 100644 --- a/libobs/obs.hpp +++ b/libobs/obs.hpp @@ -211,7 +211,7 @@ public: callback (callback_), param (param_) { - signal_handler_connect(handler, signal, callback, param); + signal_handler_connect_ref(handler, signal, callback, param); } inline void Disconnect() @@ -236,7 +236,7 @@ public: signal = signal_; callback = callback_; param = param_; - signal_handler_connect(handler, signal, callback, param); + signal_handler_connect_ref(handler, signal, callback, param); } OBSSignal(const OBSSignal&) = delete; diff --git a/libobs/obsconfig.h.in b/libobs/obsconfig.h.in index b978bdb..a959f14 100644 --- a/libobs/obsconfig.h.in +++ b/libobs/obsconfig.h.in @@ -18,6 +18,24 @@ #define BUILD_CAPTIONS @BUILD_CAPTIONS@ #define HAVE_DBUS @HAVE_DBUS@ #define HAVE_PULSEAUDIO @HAVE_PULSEAUDIO@ +#define USE_XINPUT @USE_XINPUT@ #define LIBOBS_IMAGEMAGICK_DIR_STYLE_6L 6 #define LIBOBS_IMAGEMAGICK_DIR_STYLE_7GE 7 #define LIBOBS_IMAGEMAGICK_DIR_STYLE @LIBOBS_IMAGEMAGICK_DIR_STYLE@ + +/* NOTE: Release candidate version numbers internally are always the previous + * main release number! For example, if the current public release is 21.0 and + * the build is 22.0 release candidate 1, internally the build number (defined + * by LIBOBS_API_VER/etc) will always be 21.0, despite the OBS_VERSION string + * saying "22.0 RC1". + * + * If the release candidate version number is 0.0.0 and the RC number is 0, + * that means it's not a release candidate build. */ +#define OBS_RELEASE_CANDIDATE_MAJOR @OBS_RELEASE_CANDIDATE_MAJOR@ +#define OBS_RELEASE_CANDIDATE_MINOR @OBS_RELEASE_CANDIDATE_MINOR@ +#define OBS_RELEASE_CANDIDATE_PATCH @OBS_RELEASE_CANDIDATE_PATCH@ +#define OBS_RELEASE_CANDIDATE_VER \ + MAKE_SEMANTIC_VERSION(OBS_RELEASE_CANDIDATE_MAJOR, \ + OBS_RELEASE_CANDIDATE_MINOR, \ + OBS_RELEASE_CANDIDATE_PATCH) +#define OBS_RELEASE_CANDIDATE @OBS_RELEASE_CANDIDATE@ diff --git a/libobs/util/cf-lexer.c b/libobs/util/cf-lexer.c index 8258582..0a8dd5c 100644 --- a/libobs/util/cf-lexer.c +++ b/libobs/util/cf-lexer.c @@ -76,11 +76,12 @@ char *cf_literal_to_str(const char *literal, size_t count) if (literal[0] != '\"' && literal[0] != '\'') return NULL; - str = bmalloc(count - 1); - temp_src = literal; + /* strip leading and trailing quote characters */ + str = bzalloc(--count); + temp_src = literal + 1; temp_dst = str; - while (*temp_src) { + while (*temp_src && --count > 0) { if (*temp_src == '\\') { temp_src++; cf_convert_from_escape_literal(&temp_dst, &temp_src); diff --git a/libobs/util/darray.h b/libobs/util/darray.h index 99f35d6..69e1c40 100644 --- a/libobs/util/darray.h +++ b/libobs/util/darray.h @@ -207,11 +207,13 @@ static inline void *darray_push_back_new(const size_t element_size, static inline size_t darray_push_back_array(const size_t element_size, struct darray *dst, const void *array, const size_t num) { - size_t old_num = dst->num; - - assert(array != NULL); - assert(num != 0); + size_t old_num; + if (!dst) + return 0; + if (!array || !num) + return dst->num; + old_num = dst->num; darray_resize(element_size, dst, dst->num+num); memcpy(darray_item(element_size, dst, old_num), array, element_size*num); diff --git a/libobs/util/pipe-windows.c b/libobs/util/pipe-windows.c index c3cf906..d3d35e2 100644 --- a/libobs/util/pipe-windows.c +++ b/libobs/util/pipe-windows.c @@ -50,7 +50,7 @@ static inline bool create_process(const char *cmd_line, HANDLE stdin_handle, bool success = false; si.cb = sizeof(si); - si.dwFlags = STARTF_USESTDHANDLES; + si.dwFlags = STARTF_USESTDHANDLES | STARTF_FORCEOFFFEEDBACK; si.hStdInput = stdin_handle; si.hStdOutput = stdout_handle; diff --git a/plugins/coreaudio-encoder/data/locale/gd-GB.ini b/plugins/coreaudio-encoder/data/locale/gd-GB.ini new file mode 100644 index 0000000..99e5ae0 --- /dev/null +++ b/plugins/coreaudio-encoder/data/locale/gd-GB.ini @@ -0,0 +1,2 @@ +Bitrate="Reat bhiotaichean" + diff --git a/plugins/decklink/data/locale/el-GR.ini b/plugins/decklink/data/locale/el-GR.ini index a77c8fa..26fa84c 100644 --- a/plugins/decklink/data/locale/el-GR.ini +++ b/plugins/decklink/data/locale/el-GR.ini @@ -12,4 +12,9 @@ ColorRange.Full="Πλήρης" ChannelFormat="Κανάλι" ChannelFormat.None="Κανένα" ChannelFormat.2_0ch="2 Καναλιών" +ChannelFormat.2_1ch="2.1ch" +ChannelFormat.4_0ch="4ch" +ChannelFormat.4_1ch="4.1ch" +ChannelFormat.5_1ch="5.1ch" +ChannelFormat.7_1ch="7.1ch" diff --git a/plugins/decklink/data/locale/gd-GB.ini b/plugins/decklink/data/locale/gd-GB.ini new file mode 100644 index 0000000..ecb1814 --- /dev/null +++ b/plugins/decklink/data/locale/gd-GB.ini @@ -0,0 +1,12 @@ +Device="Uidheam" +Mode="Modh" +Buffering="Cleachd bufaireadh" +ColorSpace="Spàs dhathan YUV" +ColorSpace.Default="Bun-roghainn" +ColorRange="Rainse dhathan YUV" +ColorRange.Default="Bun-roghainn" +ColorRange.Partial="Leth-phàirteach" +ColorRange.Full="Làn" +ChannelFormat="Seanail" +ChannelFormat.None="Chan eil gin" + diff --git a/plugins/decklink/data/locale/vi-VN.ini b/plugins/decklink/data/locale/vi-VN.ini index 4a63536..389e366 100644 --- a/plugins/decklink/data/locale/vi-VN.ini +++ b/plugins/decklink/data/locale/vi-VN.ini @@ -6,4 +6,9 @@ ColorRange.Full="Đầy đủ" ChannelFormat="Kênh" ChannelFormat.None="Không có" ChannelFormat.2_0ch="2ch" +ChannelFormat.2_1ch="2.1ch" +ChannelFormat.4_0ch="4ch" +ChannelFormat.4_1ch="4.1ch" +ChannelFormat.5_1ch="5.1ch" +ChannelFormat.7_1ch="7.1ch" diff --git a/plugins/image-source/data/locale/el-GR.ini b/plugins/image-source/data/locale/el-GR.ini index 5922cbf..952ab1b 100644 --- a/plugins/image-source/data/locale/el-GR.ini +++ b/plugins/image-source/data/locale/el-GR.ini @@ -9,11 +9,25 @@ SlideShow.Files="Αρχεία εικόνων" SlideShow.CustomSize="Μέγεθος Οριοθέτησης/Αναλογία" SlideShow.CustomSize.Auto="Αυτόματο" SlideShow.Randomize="Τυχαιοποίηση αναπαραγωγής" +SlideShow.Loop="Επανάληψη" SlideShow.Transition="Μετάβαση" SlideShow.Transition.Cut="Αποκοπή" SlideShow.Transition.Fade="Ξεθώριασμα" SlideShow.Transition.Swipe="Σύρσιμο" SlideShow.Transition.Slide="Ολίσθηση" +SlideShow.PlaybackBehavior="Συμπεριφορά Ορατότητας" +SlideShow.PlaybackBehavior.StopRestart="Διακοπή όταν δεν είναι ορατή, επανεκκίνηση όταν είναι ορατή" +SlideShow.PlaybackBehavior.PauseUnpause="Παύση όταν δεν είναι ορατή, συνέχεια όταν είναι ορατή" +SlideShow.PlaybackBehavior.AlwaysPlay="Αναπαραγωγή πάντα, ακόμα και αν δεν είναι ορατή" +SlideShow.SlideMode="Λειτουργία παρουσίασης διαφανειών" +SlideShow.SlideMode.Auto="Αυτόματα" +SlideShow.SlideMode.Manual="Χειροκίνητα (χρήση συντομέυσεων για τον έλεγχο slideshow)" +SlideShow.PlayPause="Αναπαραγωγή/Παύση" +SlideShow.Restart="Επανεκκίνηση" +SlideShow.Stop="Διακοπή" +SlideShow.NextSlide="Επόμενη διαφάνεια" +SlideShow.PreviousSlide="Προηγούμενη διαφάνεια" +SlideShow.HideWhenDone="Απόκρυψη όταν η παρουσίαση τελειώσει" ColorSource="Πηγή χρώματος" ColorSource.Color="Χρώμα" diff --git a/plugins/image-source/data/locale/et-EE.ini b/plugins/image-source/data/locale/et-EE.ini index e4b1792..cec51a4 100644 --- a/plugins/image-source/data/locale/et-EE.ini +++ b/plugins/image-source/data/locale/et-EE.ini @@ -6,12 +6,17 @@ SlideShow="Piltide slaidiesitus" SlideShow.TransitionSpeed="Ülemineku kiirus (millisekundites)" SlideShow.SlideTime="Aeg slaidide vahel (millisekundites)" SlideShow.Files="Pildifailid" +SlideShow.CustomSize.Auto="Automaatne" SlideShow.Randomize="Juhuslik taasesitus" +SlideShow.Loop="Korda" SlideShow.Transition="Üleminek" SlideShow.Transition.Cut="Ilmuv" SlideShow.Transition.Fade="Hajuv" SlideShow.Transition.Swipe="Pühkiv" SlideShow.Transition.Slide="Sisselendav" +SlideShow.PlaybackBehavior.AlwaysPlay="Mängi alati, isegi siis kui pole nähtav" +SlideShow.SlideMode.Auto="Automaatne" +SlideShow.PlayPause="Esita/Peata" ColorSource="Värvi allikas" ColorSource.Color="Värv" diff --git a/plugins/image-source/data/locale/gd-GB.ini b/plugins/image-source/data/locale/gd-GB.ini new file mode 100644 index 0000000..7174379 --- /dev/null +++ b/plugins/image-source/data/locale/gd-GB.ini @@ -0,0 +1,25 @@ +ImageInput="Dealbh" +File="Faidhle deilbh" +UnloadWhenNotShowing="Dì-luchdaich an dealbh mur eil e ’ga shealltainn" + +SlideShow="Taisbeanadh shleamhnagan dhealbhan" +SlideShow.TransitionSpeed="Luaths an tar-mhùthaidh (mille-dhiog)" +SlideShow.SlideTime="An ùine eadar na sleamhnagan (mille-dhiog)" +SlideShow.Files="Faidhlichean deilbh" +SlideShow.CustomSize="Meud an iadhaidh/Co-mheas deilbh" +SlideShow.CustomSize.Auto="Fèin-obrachail" +SlideShow.Loop="Lùb" +SlideShow.Transition="Tar-mhùthadh" +SlideShow.Transition.Cut="Gearr às" +SlideShow.Transition.Fade="Crìon" +SlideShow.Transition.Swipe="Grad-shlaighd" +SlideShow.Transition.Slide="Sleamhnaich" +SlideShow.SlideMode.Auto="Fèin-obrachail" +SlideShow.PlayPause="Cluich/Cuir ’na stad" +SlideShow.Restart="Ath-thòisich" +SlideShow.Stop="Cuir stad air" + +ColorSource.Color="Dath" +ColorSource.Width="Leud" +ColorSource.Height="Àirde" + diff --git a/plugins/linux-alsa/data/locale/gd-GB.ini b/plugins/linux-alsa/data/locale/gd-GB.ini new file mode 100644 index 0000000..4b3c24c --- /dev/null +++ b/plugins/linux-alsa/data/locale/gd-GB.ini @@ -0,0 +1,2 @@ +Device="Uidheam" + diff --git a/plugins/linux-capture/data/locale/gd-GB.ini b/plugins/linux-capture/data/locale/gd-GB.ini new file mode 100644 index 0000000..67871f3 --- /dev/null +++ b/plugins/linux-capture/data/locale/gd-GB.ini @@ -0,0 +1,16 @@ +X11SharedMemoryScreenInput="Glacadh-sgrìn (XSHM)" +Screen="Sgrìn" +CaptureCursor="Glac an cùrsair" +AdvancedSettings="Roghainnean adhartach" +XServer="Frithealaiche X" +XCCapture="Glacadh uinneige (Xcomposite)" +Window="Uinneag" +CropTop="Bearr aig a’ bharr (piogsail)" +CropLeft="Bearr aig an taobh chlì (piogsail)" +CropRight="Bearr aig an taobh deas (piogsail)" +CropBottom="Bearr aig a’ bhonn (piogsail)" +SwapRedBlue="Suaip dearg is gorm" +LockX="Glais am frithealaiche X rè a’ ghlacaidh" +IncludeXBorder="Gabh a-steach an t-iomall X" +ExcludeAlpha="Cleachd fòrmat innich gun alpha (seachnadh buga Mesa)" + diff --git a/plugins/linux-pulseaudio/data/locale/gd-GB.ini b/plugins/linux-pulseaudio/data/locale/gd-GB.ini new file mode 100644 index 0000000..3a2f3e9 --- /dev/null +++ b/plugins/linux-pulseaudio/data/locale/gd-GB.ini @@ -0,0 +1,4 @@ +PulseInput="Glacadh ion-chur fuaime (PulseAudio)" +PulseOutput="Glacadh às-chur fuaime (PulseAudio)" +Device="Uidheam" + diff --git a/plugins/linux-pulseaudio/data/locale/pt-BR.ini b/plugins/linux-pulseaudio/data/locale/pt-BR.ini index be20b60..b67b8f0 100644 --- a/plugins/linux-pulseaudio/data/locale/pt-BR.ini +++ b/plugins/linux-pulseaudio/data/locale/pt-BR.ini @@ -1,4 +1,4 @@ PulseInput="Dispositivo de Entrada de Áudio (PulseAudio)" -PulseOutput="Dispotitivo de Saída de Áudio (PulseAudio)" -Device="Dispotivo" +PulseOutput="Dispositivo de Saída de Áudio (PulseAudio)" +Device="Dispositivo" diff --git a/plugins/linux-v4l2/data/locale/gd-GB.ini b/plugins/linux-v4l2/data/locale/gd-GB.ini new file mode 100644 index 0000000..78a1c70 --- /dev/null +++ b/plugins/linux-v4l2/data/locale/gd-GB.ini @@ -0,0 +1,11 @@ +V4L2Input="Uidheam glacadh video (V4L2)" +Device="Uidheam" +Input="Ion-chur" +VideoFormat="Fòrmat a’ video" +VideoStandard="Stannard a’ video" +DVTiming="Co-thìmeachadh DV" +Resolution="Dùmhlachd-bhreacaidh" +FrameRate="Reat fhrèamaichean" +LeaveUnchanged="Cum gun atharrachadh" +UseBuffering="Cleachd bufaireadh" + diff --git a/plugins/mac-avcapture/data/locale/gd-GB.ini b/plugins/mac-avcapture/data/locale/gd-GB.ini new file mode 100644 index 0000000..ae9918b --- /dev/null +++ b/plugins/mac-avcapture/data/locale/gd-GB.ini @@ -0,0 +1,14 @@ +AVCapture="Uidheam glacadh video" +Device="Uidheam" +UsePreset="Cleachd ro-sheata" +Preset="Ro-sheata" +Buffering="Cleachd bufaireadh" +FrameRate="Reat fhrèamaichean" +InputFormat="Fòrmat an ion-chuir" +ColorSpace="Spàs dhathan" +VideoRange="Rainse a’ video" +VideoRange.Partial="Leth-phàirteach" +VideoRange.Full="Làn" +Auto="Fèin-obrachail" +Unknown="Chan eil fhios ($1)" + diff --git a/plugins/mac-avcapture/data/locale/ka-GE.ini b/plugins/mac-avcapture/data/locale/ka-GE.ini new file mode 100644 index 0000000..d00fdbd --- /dev/null +++ b/plugins/mac-avcapture/data/locale/ka-GE.ini @@ -0,0 +1,14 @@ +AVCapture="ვიდეოს ჩამწერი მოწყობილობა" +Device="მოწყობილობა" +UsePreset="მზა პარამეტრებით სარგებლობა" +Preset="მზა პარამეტრები" +Buffering="ბუფერიზაციის გამოყენება" +FrameRate="კადრის სიხშირე" +InputFormat="შეტანის ფორმატი" +ColorSpace="ფერთა სისტემა" +VideoRange="ვიდეოების არე" +VideoRange.Partial="ნაწილობრივი" +VideoRange.Full="სრული" +Auto="ავტომატური" +Unknown="უცნობი ($1)" + diff --git a/plugins/mac-capture/data/locale/gd-GB.ini b/plugins/mac-capture/data/locale/gd-GB.ini new file mode 100644 index 0000000..b4cf88f --- /dev/null +++ b/plugins/mac-capture/data/locale/gd-GB.ini @@ -0,0 +1,21 @@ +CoreAudio.InputCapture="Glacadh ion-chur fuaime" +CoreAudio.OutputCapture="Glacadh às-chur fuaime" +CoreAudio.Device="Uidheam" +CoreAudio.Device.Default="Bun-roghainn" +DisplayCapture="Glacadh uidheim-taisbeanaidh" +DisplayCapture.Display="Uidheam-taisbeanaidh" +DisplayCapture.ShowCursor="Seall an cùrsair" +WindowCapture="Glacadh uinneige" +WindowCapture.ShowShadow="Seall sgàil na h-uinneige" +WindowUtils.Window="Uinneag" +WindowUtils.ShowEmptyNames="Seall na h-uinneagan le ainmean falamh" +CropMode="Bearr" +CropMode.None="Chan eil gin" +CropMode.Manual="A làimh" +CropMode.ToWindow="Dhan uinneag" +CropMode.ToWindowAndManual="Dhan uinneag is a làimh" +Crop.origin.x="Bearr aig an taobh chlì" +Crop.origin.y="Bearr aig a’ bharr" +Crop.size.width="Bearr aig an taobh deas" +Crop.size.height="Bearr aig a’ bhonn" + diff --git a/plugins/mac-capture/data/locale/ka-GE.ini b/plugins/mac-capture/data/locale/ka-GE.ini new file mode 100644 index 0000000..0f8b3eb --- /dev/null +++ b/plugins/mac-capture/data/locale/ka-GE.ini @@ -0,0 +1,21 @@ +CoreAudio.InputCapture="შემავალი ხმოვანი სიგნალის ჩაწერა" +CoreAudio.OutputCapture="გამომავალი ხმოვანი სიგნალის ჩაწერა" +CoreAudio.Device="მოწყობილობა" +CoreAudio.Device.Default="ნაგულისხმევი" +DisplayCapture="ეკრანის გადაღება" +DisplayCapture.Display="ეკრანი" +DisplayCapture.ShowCursor="მაჩვენებლის გამოჩენა" +WindowCapture="ფანჯრის გადაღება" +WindowCapture.ShowShadow="ფანჯრის ჩრდილის ჩვენება" +WindowUtils.Window="ფანჯარა" +WindowUtils.ShowEmptyNames="ფანჯრის ჩვენება ცარიელი სახელებით" +CropMode="შემოჭრა" +CropMode.None="არცერთი" +CropMode.Manual="ხელით" +CropMode.ToWindow="ფანჯარაზე" +CropMode.ToWindowAndManual="ფანჯარაზე ხელით" +Crop.origin.x="მარცხნივ შემოჭრა" +Crop.origin.y="ზემოთ შემოჭრა" +Crop.size.width="მარჯვნივ შემოჭრა" +Crop.size.height="ქვემოთ შემოჭრა" + diff --git a/plugins/mac-syphon/data/locale/gd-GB.ini b/plugins/mac-syphon/data/locale/gd-GB.ini new file mode 100644 index 0000000..cae49db --- /dev/null +++ b/plugins/mac-syphon/data/locale/gd-GB.ini @@ -0,0 +1,9 @@ +Source="Tùs" +Application="Aplacaid" +Crop="Bearr" +Crop.origin.x="Bearr aig an taobh chlì" +Crop.origin.y="Bearr aig a’ bharr" +Crop.size.width="Bearr aig an taobh deas" +Crop.size.height="Bearr aig a’ bhonn" +AllowTransparency="Ceadaich trìd-shoillearachd" + diff --git a/plugins/mac-syphon/data/locale/ka-GE.ini b/plugins/mac-syphon/data/locale/ka-GE.ini new file mode 100644 index 0000000..f359593 --- /dev/null +++ b/plugins/mac-syphon/data/locale/ka-GE.ini @@ -0,0 +1,13 @@ +Syphon="თამაშის გადაღება (Syphon)" +Source="წყარო" +LaunchSyphonInject="SyphonInject გაშვება" +Inject="ჩადგმა" +Application="პროგრამა" +SyphonLicense="Syphon ლიცენზია" +Crop="შემოჭრა" +Crop.origin.x="მარცხნივ შემოჭრა" +Crop.origin.y="ზემოთ შემოჭრა" +Crop.size.width="მარჯვნივ შემოჭრა" +Crop.size.height="ქვემოთ შემოჭრა" +AllowTransparency="გამჭვირვალობის დაშვება" + diff --git a/plugins/mac-vth264/data/locale/de-DE.ini b/plugins/mac-vth264/data/locale/de-DE.ini index b391d62..b095bd3 100644 --- a/plugins/mac-vth264/data/locale/de-DE.ini +++ b/plugins/mac-vth264/data/locale/de-DE.ini @@ -4,7 +4,7 @@ VTEncoder="VideoToolbox Codierer" Bitrate="Bitrate" UseMaxBitrate="Limitiere Bitrate" MaxBitrate="Maximale Bitrate" -MaxBitrateWindow="Maximale Bitrate Fenster (Sekunden)" +MaxBitrateWindow="Maximales Bitrate Fenster (Sekunden)" KeyframeIntervalSec="Keyframeintervall (Sekunden, 0=auto)" Profile="Profil" None="(Nichts)" diff --git a/plugins/mac-vth264/data/locale/gd-GB.ini b/plugins/mac-vth264/data/locale/gd-GB.ini new file mode 100644 index 0000000..503292a --- /dev/null +++ b/plugins/mac-vth264/data/locale/gd-GB.ini @@ -0,0 +1,6 @@ +Bitrate="Reat bhiotaichean" +KeyframeIntervalSec="Eadaramh nam frèamaichean-iuchrach (diog, fèin-obrachail)" +Profile="Pròifil" +None="(Chan eil gin)" + + diff --git a/plugins/mac-vth264/data/locale/ka-GE.ini b/plugins/mac-vth264/data/locale/ka-GE.ini new file mode 100644 index 0000000..c6855e8 --- /dev/null +++ b/plugins/mac-vth264/data/locale/ka-GE.ini @@ -0,0 +1,14 @@ +VTH264EncHW="Apple VT H264 აპარატურული დამშიფრავი" +VTH264EncSW="Apple VT H264 პროგრამული დამშიფრავი" +VTEncoder="VideoToolbox დამშიფრავი" +Bitrate="ბიტური სიხშირე" +UseMaxBitrate="ბიტური სიხშირის შეზღუდვა" +MaxBitrate="უმაღლესი დაშვებული ბიტური სიხშირის" +MaxBitrateWindow="უმაღლესი ბიტური სიხშირის ფანჯარა (წამები)" +KeyframeIntervalSec="საკვანძო კადრებს შორის შუალედი (წამი, 0=თვითშერჩევა)" +Profile="პროფილი" +None="(არცერთი)" +DefaultEncoder="(ნაგულისხმევი დამშიფრავი)" +UseBFrames="B-კადრების გამოყენება" + + diff --git a/plugins/mac-vth264/encoder.c b/plugins/mac-vth264/encoder.c index f5bb5e4..71e5e89 100644 --- a/plugins/mac-vth264/encoder.c +++ b/plugins/mac-vth264/encoder.c @@ -424,11 +424,12 @@ static void vt_h264_video_info(void *data, struct video_scale_info *info) enc->vt_pix_fmt = enc->fullrange ? kCVPixelFormatType_420YpCbCr8PlanarFullRange : kCVPixelFormatType_420YpCbCr8Planar; - } else if (info->format == VIDEO_FORMAT_I444) { - enc->obs_pix_fmt = info->format; - enc->vt_pix_fmt = kCVPixelFormatType_444YpCbCr10; + return; } + if (info->format == VIDEO_FORMAT_I444) + VT_BLOG(LOG_WARNING, "I444 color format not supported"); + // Anything else, return default enc->obs_pix_fmt = VIDEO_FORMAT_NV12; enc->vt_pix_fmt = enc->fullrange ? @@ -445,11 +446,12 @@ static void update_params(struct vt_h264_encoder *enc, obs_data_t *settings) struct video_scale_info info = { .format = voi->format }; + enc->fullrange = voi->range == VIDEO_RANGE_FULL; + // also sets the enc->vt_pix_fmt vt_h264_video_info(enc, &info); enc->colorspace = voi->colorspace; - enc->fullrange = voi->range == VIDEO_RANGE_FULL; enc->width = obs_encoder_get_width(enc->encoder); enc->height = obs_encoder_get_height(enc->encoder); diff --git a/plugins/obs-ffmpeg/data/locale/cs-CZ.ini b/plugins/obs-ffmpeg/data/locale/cs-CZ.ini index 3561fdd..5f688a3 100644 --- a/plugins/obs-ffmpeg/data/locale/cs-CZ.ini +++ b/plugins/obs-ffmpeg/data/locale/cs-CZ.ini @@ -36,6 +36,7 @@ ColorRange.Auto="Automatický" ColorRange.Partial="Částečný" ColorRange.Full="Celkový" RestartMedia="Restartovat mediální zdroj" +SpeedPercentage="Rychlost (procenta)" Seekable="Posouvatelné" MediaFileFilter.AllMediaFiles="Všechny mediální soubory" diff --git a/plugins/obs-ffmpeg/data/locale/da-DK.ini b/plugins/obs-ffmpeg/data/locale/da-DK.ini index 892fd4a..25277c1 100644 --- a/plugins/obs-ffmpeg/data/locale/da-DK.ini +++ b/plugins/obs-ffmpeg/data/locale/da-DK.ini @@ -36,6 +36,7 @@ ColorRange.Auto="Auto" ColorRange.Partial="Delvis" ColorRange.Full="Fuld" RestartMedia="Genstart Media" +SpeedPercentage="Hastighed (procent)" Seekable="Seekable" MediaFileFilter.AllMediaFiles="Alle mediefiler" diff --git a/plugins/obs-ffmpeg/data/locale/el-GR.ini b/plugins/obs-ffmpeg/data/locale/el-GR.ini index 7a224d7..77562dc 100644 --- a/plugins/obs-ffmpeg/data/locale/el-GR.ini +++ b/plugins/obs-ffmpeg/data/locale/el-GR.ini @@ -1,5 +1,6 @@ FFmpegOutput="Έξοδος FFmpeg" FFmpegAAC="FFmpeg προεπιλεγμένος κωδικοποιητής AAC" +FFmpegOpus="FFmpeg Opus κωδικοποιητής" Bitrate="Ρυθμός μετάδοσης bit" Preset="Προκαθορισμένο στυλ" RateControl="Έλεγχος ρυθμού" @@ -35,6 +36,8 @@ ColorRange.Auto="Αυτόματο" ColorRange.Partial="Μερικός" ColorRange.Full="Πλήρης" RestartMedia="Επανεκκίνηση Πολυμέσων" +SpeedPercentage="Ταχύτητα (τοις εκατό)" +Seekable="Παρεχόμενη" MediaFileFilter.AllMediaFiles="Όλα τα αρχεία πολυμέσων" MediaFileFilter.VideoFiles="Αρχεία Βίντεο" @@ -44,4 +47,6 @@ MediaFileFilter.AllFiles="Όλα τα αρχεία" ReplayBuffer="Διάρκεια μνήμης Replay" ReplayBuffer.Save="Αποθήκευση Replay" +HelperProcessFailed="Αδυναμία έναρξης τού βοηθού της διαδικασίας εγγραφής. Ελέγξτε ότι τα αρχεία OBS δεν έχουν αποκλειστεί η καταργηθεί από ένα τρίτο antivirus / λογισμικό ασφαλείας." +UnableToWritePath="Αδυναμία εγγραφής %1. Βεβαιωθείτε ότι χρησιμοποιείτε μια διαδρομή εγγραφής που επιτρέπεται για εγγραφή τού λογαριασμού χρήστη και ότι υπάρχει επαρκής χώρος στον σκληρό δίσκο." diff --git a/plugins/obs-ffmpeg/data/locale/es-ES.ini b/plugins/obs-ffmpeg/data/locale/es-ES.ini index f7999d4..9dcb1cf 100644 --- a/plugins/obs-ffmpeg/data/locale/es-ES.ini +++ b/plugins/obs-ffmpeg/data/locale/es-ES.ini @@ -36,6 +36,7 @@ ColorRange.Auto="Automatico" ColorRange.Partial="Parcial" ColorRange.Full="Completo" RestartMedia="Reiniciar Medio" +SpeedPercentage="Velocidad (porcentaje)" Seekable="Buscable" MediaFileFilter.AllMediaFiles="Todos los archivos multimedia" diff --git a/plugins/obs-ffmpeg/data/locale/eu-ES.ini b/plugins/obs-ffmpeg/data/locale/eu-ES.ini index 2b100b1..13d38a1 100644 --- a/plugins/obs-ffmpeg/data/locale/eu-ES.ini +++ b/plugins/obs-ffmpeg/data/locale/eu-ES.ini @@ -36,6 +36,7 @@ ColorRange.Auto="Auto" ColorRange.Partial="Partziala" ColorRange.Full="Osoa" RestartMedia="Berrabiarazi euskarria" +SpeedPercentage="Abiadura (ehunekoa)" Seekable="Bilagai" MediaFileFilter.AllMediaFiles="Multimedia-fitxategi guztiak" diff --git a/plugins/obs-ffmpeg/data/locale/gd-GB.ini b/plugins/obs-ffmpeg/data/locale/gd-GB.ini new file mode 100644 index 0000000..8c48562 --- /dev/null +++ b/plugins/obs-ffmpeg/data/locale/gd-GB.ini @@ -0,0 +1,49 @@ +FFmpegOutput="Às-chur FFmpeg" +FFmpegAAC="Inneal-còdachaidh AAC tùsail airson FFmpeg" +FFmpegOpus="Inneal-còdachaidh Opus airson FFmpeg" +Bitrate="Reat bhiotaichean" +Preset="Ro-sheata" +RateControl="Smachd air an reat" +KeyframeIntervalSec="Eadaramh nam frèamaichean-iuchrach (diog, fèin-obrachail)" +Lossless="Gun chall càileachd" + +BFrames="Frèamaichean-B" + +NVENC.Use2Pass="Cleachd còdachadh dà phas" +NVENC.Preset.default="Bun-roghainn" +NVENC.Preset.hq="Càileachd àrd" +NVENC.Preset.hp="Dèanadas àrd" +NVENC.Preset.bd="Bluray" +NVENC.Preset.ll="Foillidheachd ìosal" +NVENC.Preset.llhq="Foillidheachd ìosal ⁊ càileachd àrd" +NVENC.Preset.llhp="Foillidheachd ìosal ⁊ dèanadas àrd" +NVENC.Level="Leibheil" + +FFmpegSource="Tùs a’ mheadhain" +LocalFile="Faidhle ionadail" +Looping="Lùb" +Input="Ion-chur" +InputFormat="Fòrmat an ion-chuir" +BufferingMB="Bufair an lìonraidh (MB)" +HardwareDecode="Cleachd dì-chòdachadh bathair-chruaidh ma bhios e ri fhaighinn" +ClearOnMediaEnd="Falaich an tùs nuair a bhios a’ chluiche deiseil" +Advanced="Adhartach" +RestartWhenActivated="Ath-thòisich a’ chluiche nuair a thig gnìomh on tùs" +CloseFileWhenInactive="Dùin am faidhle mur eil gnìomh ann" +ColorRange="Rainse dhathan YUV" +ColorRange.Auto="Fèin-obrachail" +ColorRange.Partial="Leth-phàirteach" +ColorRange.Full="Làn" +RestartMedia="Ath-thòisich am meadhan" +SpeedPercentage="Luaths (sa cheud)" +Seekable="Gabhaidh sireadh ann" + +MediaFileFilter.AllMediaFiles="A h-uile faidhle meadhain" +MediaFileFilter.VideoFiles="Faidhlichean video" +MediaFileFilter.AudioFiles="Faidhlichean fuaime" +MediaFileFilter.AllFiles="A h-uile faidhle" + +ReplayBuffer="Bufair na h-ath-chluiche" +ReplayBuffer.Save="Sàbhail an ath-chluiche" + + diff --git a/plugins/obs-ffmpeg/data/locale/it-IT.ini b/plugins/obs-ffmpeg/data/locale/it-IT.ini index 623ba5a..2d69185 100644 --- a/plugins/obs-ffmpeg/data/locale/it-IT.ini +++ b/plugins/obs-ffmpeg/data/locale/it-IT.ini @@ -36,6 +36,7 @@ ColorRange.Auto="Autom." ColorRange.Partial="Parziale" ColorRange.Full="Intero" RestartMedia="Riavvia Media" +SpeedPercentage="Velocità (percentuale)" Seekable="Ricercabile" MediaFileFilter.AllMediaFiles="Tutti i file media" diff --git a/plugins/obs-ffmpeg/data/locale/ka-GE.ini b/plugins/obs-ffmpeg/data/locale/ka-GE.ini new file mode 100644 index 0000000..f1d9220 --- /dev/null +++ b/plugins/obs-ffmpeg/data/locale/ka-GE.ini @@ -0,0 +1,52 @@ +FFmpegOutput="FFmpeg გამომავალი სიგნალი" +FFmpegAAC="FFmpeg ნაგულისხმევი AAC დამშიფრავი" +FFmpegOpus="FFmpeg Opus დამშიფრავი" +Bitrate="ბიტური სიხშირე" +Preset="მზა პარამეტრები" +RateControl="სიხშირის მართვა" +KeyframeIntervalSec="საკვანძო კადრებს შორის შუალედი (წამი, 0=თვითშერჩევა)" +Lossless="უდანაკარგო" + +BFrames="B-კადრები" + +NVENC.Use2Pass="ორმაგი დაშიფვრის გამოყენება" +NVENC.Preset.default="ნაგულისხმევი" +NVENC.Preset.hq="მაღალი ხარისხი" +NVENC.Preset.hp="მაღალი წარმადობა" +NVENC.Preset.bd="Bluray" +NVENC.Preset.ll="მცირე დაყოვნება" +NVENC.Preset.llhq="მცირე დაყოვნება, მაღალი ხარისხი" +NVENC.Preset.llhp="მცირე დაყოვნება, მაღალი წარმადობა" +NVENC.Level="საფეხური" + +FFmpegSource="მასალის წყარო" +LocalFile="ადგილობრივი ფაილი" +Looping="დაუსრულებლად გამეორება" +Input="შეტანა" +InputFormat="შეტანის ფორმატი" +BufferingMB="ქსელის ბუფერიზაცია (მბაიტი)" +HardwareDecode="აპარატურული დაშიფვრის გამოყენება, ხელმისაწვდომობის შემთხვევაში" +ClearOnMediaEnd="წყაროს დამალვა, გაშვების დამთავრებისას" +Advanced="გაფართოებული" +RestartWhenActivated="ხელახლა გაშვება წყაროს ამოქმედებისას" +CloseFileWhenInactive="ფაილის დახურვა უმოქმედობისას" +CloseFileWhenInactive.ToolTip="ფაილი დაიხურება, თუ წყარო არ იქნება ეთერში ან\nჩაწერაზე გაშვებული. ეს საშუალებას იძლევა შეიცვალოს ფაილი, როცა წყარო არაა მოქმედი,\nთუმცა ხელახლა ამოქმედებისას, შესაძლოა გარკვეული დროით დაყოვნებას ჰქონდეს ადგილი." +ColorRange="YUV ფერთა გამა" +ColorRange.Auto="ავტომატური" +ColorRange.Partial="ნაწილობრივი" +ColorRange.Full="სრული" +RestartMedia="მასალის ხელახლა გაშვება" +SpeedPercentage="სიჩქარე (პროცენტი)" +Seekable="გადახვევით" + +MediaFileFilter.AllMediaFiles="ყველა მასალა" +MediaFileFilter.VideoFiles="ვიდეოფაილები" +MediaFileFilter.AudioFiles="ხმოვანი ფაილები" +MediaFileFilter.AllFiles="ყველა ფაილი" + +ReplayBuffer="გადახვევა" +ReplayBuffer.Save="გადახვევის შენახვა" + +HelperProcessFailed="ჩაწერის პროცესის გაშვება ვერ ხერხდება. გადაამოწმეთ, ხომ არ არის OBS ფაილები შეზღუდული ან წაშლილი ანტივირუსის / უსაფრთხოების სხვა პროგრამების მიერ." +UnableToWritePath="%1-ში ჩაწერა ვერ ხერხდება. დარწმუნდით, რომ ჩაწერისთვის ისეთი მისამართი გაქვთ მითითებული, სადაც ჩაწერის ნებართვაც გააჩნია თქვენს ანგარიშს და ამასთან, არის საკმარისი ადგილი დისკზე." + diff --git a/plugins/obs-ffmpeg/data/locale/nb-NO.ini b/plugins/obs-ffmpeg/data/locale/nb-NO.ini index 38ad66c..7439c6e 100644 --- a/plugins/obs-ffmpeg/data/locale/nb-NO.ini +++ b/plugins/obs-ffmpeg/data/locale/nb-NO.ini @@ -36,6 +36,7 @@ ColorRange.Auto="Automatisk" ColorRange.Partial="Delvis" ColorRange.Full="Hel" RestartMedia="Start media på nytt" +SpeedPercentage="Fart (prosent)" Seekable="Søkbar" MediaFileFilter.AllMediaFiles="Alle mediefiler" diff --git a/plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c b/plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c index 755b67c..b3c5c88 100644 --- a/plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c +++ b/plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c @@ -316,6 +316,7 @@ static void create_video_stream(struct ffmpeg_mux *ffm) (AVRational){ffm->params.fps_den, ffm->params.fps_num}; ffm->video_stream->time_base = context->time_base; + ffm->video_stream->avg_frame_rate = av_inv_q(context->time_base); if (ffm->output->oformat->flags & AVFMT_GLOBALHEADER) context->flags |= CODEC_FLAG_GLOBAL_H; diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-audio-encoders.c b/plugins/obs-ffmpeg/obs-ffmpeg-audio-encoders.c index 31b0e9b..7d6c5ae 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-audio-encoders.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg-audio-encoders.c @@ -132,6 +132,10 @@ static bool initialize_codec(struct enc_encoder *enc) warn("Failed to open AAC codec: %s", av_err2str(ret)); return false; } + enc->aframe->format = enc->context->sample_fmt; + enc->aframe->channels = enc->context->channels; + enc->aframe->channel_layout = enc->context->channel_layout; + enc->aframe->sample_rate = enc->context->sample_rate; enc->frame_size = enc->context->frame_size; if (!enc->frame_size) diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-output.c b/plugins/obs-ffmpeg/obs-ffmpeg-output.c index facc171..9acdef2 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-output.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg-output.c @@ -290,6 +290,11 @@ static bool open_audio_codec(struct ffmpeg_data *data) return false; } + data->aframe->format = context->sample_fmt; + data->aframe->channels = context->channels; + data->aframe->channel_layout = context->channel_layout; + data->aframe->sample_rate = context->sample_rate; + context->strict_std_compliance = -2; ret = avcodec_open2(context, data->acodec, NULL); diff --git a/plugins/obs-filters/color-correction-filter.c b/plugins/obs-filters/color-correction-filter.c index e418d23..fad36a6 100644 --- a/plugins/obs-filters/color-correction-filter.c +++ b/plugins/obs-filters/color-correction-filter.c @@ -351,16 +351,16 @@ static obs_properties_t *color_correction_filter_properties(void *data) obs_properties_t *props = obs_properties_create(); obs_properties_add_float_slider(props, SETTING_GAMMA, - TEXT_GAMMA, -3.0f, 3.0f, 0.01f); + TEXT_GAMMA, -3.0, 3.0, 0.01); obs_properties_add_float_slider(props, SETTING_CONTRAST, - TEXT_CONTRAST, -2.0f, 2.0f, 0.01f); + TEXT_CONTRAST, -2.0, 2.0, 0.01); obs_properties_add_float_slider(props, SETTING_BRIGHTNESS, - TEXT_BRIGHTNESS, -1.0f, 1.0f, 0.01f); + TEXT_BRIGHTNESS, -1.0, 1.0, 0.01); obs_properties_add_float_slider(props, SETTING_SATURATION, - TEXT_SATURATION, -1.0f, 5.0f, 0.01f); + TEXT_SATURATION, -1.0, 5.0, 0.01); obs_properties_add_float_slider(props, SETTING_HUESHIFT, - TEXT_HUESHIFT, -180.0f, 180.0f, 0.01f); + TEXT_HUESHIFT, -180.0, 180.0, 0.01); obs_properties_add_int_slider(props, SETTING_OPACITY, TEXT_OPACITY, 0, 100, 1); diff --git a/plugins/obs-filters/compressor-filter.c b/plugins/obs-filters/compressor-filter.c index 3c3789f..244a9bc 100644 --- a/plugins/obs-filters/compressor-filter.c +++ b/plugins/obs-filters/compressor-filter.c @@ -40,12 +40,12 @@ #define TEXT_OUTPUT_GAIN MT_("Compressor.OutputGain") #define TEXT_SIDECHAIN_SOURCE MT_("Compressor.SidechainSource") -#define MIN_RATIO 1.0f -#define MAX_RATIO 32.0f -#define MIN_THRESHOLD_DB -60.0f +#define MIN_RATIO 1.0 +#define MAX_RATIO 32.0 +#define MIN_THRESHOLD_DB -60.0 #define MAX_THRESHOLD_DB 0.0f -#define MIN_OUTPUT_GAIN_DB -32.0f -#define MAX_OUTPUT_GAIN_DB 32.0f +#define MIN_OUTPUT_GAIN_DB -32.0 +#define MAX_OUTPUT_GAIN_DB 32.0 #define MIN_ATK_RLS_MS 1 #define MAX_RLS_MS 1000 #define MAX_ATK_MS 500 @@ -442,6 +442,9 @@ static struct obs_audio_data *compressor_filter_audio(void *data, struct compressor_data *cd = data; const uint32_t num_samples = audio->frames; + if (num_samples == 0) + return audio; + float **samples = (float**)audio->data; pthread_mutex_lock(&cd->sidechain_update_mutex); @@ -497,15 +500,15 @@ static obs_properties_t *compressor_properties(void *data) parent = obs_filter_get_parent(cd->context); obs_properties_add_float_slider(props, S_RATIO, - TEXT_RATIO, MIN_RATIO, MAX_RATIO, 0.5f); + TEXT_RATIO, MIN_RATIO, MAX_RATIO, 0.5); obs_properties_add_float_slider(props, S_THRESHOLD, - TEXT_THRESHOLD, MIN_THRESHOLD_DB, MAX_THRESHOLD_DB, 0.1f); + TEXT_THRESHOLD, MIN_THRESHOLD_DB, MAX_THRESHOLD_DB, 0.1); obs_properties_add_int_slider(props, S_ATTACK_TIME, TEXT_ATTACK_TIME, MIN_ATK_RLS_MS, MAX_ATK_MS, 1); obs_properties_add_int_slider(props, S_RELEASE_TIME, TEXT_RELEASE_TIME, MIN_ATK_RLS_MS, MAX_RLS_MS, 1); obs_properties_add_float_slider(props, S_OUTPUT_GAIN, - TEXT_OUTPUT_GAIN, MIN_OUTPUT_GAIN_DB, MAX_OUTPUT_GAIN_DB, 0.1f); + TEXT_OUTPUT_GAIN, MIN_OUTPUT_GAIN_DB, MAX_OUTPUT_GAIN_DB, 0.1); obs_property_t *sources = obs_properties_add_list(props, S_SIDECHAIN_SOURCE, TEXT_SIDECHAIN_SOURCE, diff --git a/plugins/obs-filters/data/locale/el-GR.ini b/plugins/obs-filters/data/locale/el-GR.ini index 54d2059..dd23c14 100644 --- a/plugins/obs-filters/data/locale/el-GR.ini +++ b/plugins/obs-filters/data/locale/el-GR.ini @@ -1,15 +1,25 @@ ColorFilter="Διόρθωση Χρώματος" ColorGradeFilter="Εφαρμογή LUT" +MaskFilter="Μάσκα εικόνα/μείγμα" AsyncDelayFilter="Καθυστέρηση Βίντεο (Ασύγχρονη)" +CropFilter="Αποκοπή/Pad" ScrollFilter="Κύλιση" ChromaKeyFilter="Κλειδί Chroma" ColorKeyFilter="Κλειδί Χρώματος" SharpnessFilter="Όξυνση" +ScaleFilter="Κλιμάκωση/αναλογίες" +GPUDelayFilter="Καταστήσει καθυστέρηση" +UndistortCenter="Undistort κέντρο της εικόνας κατά την κλιμάκωση από ultrawide" +NoiseGate="Πύλη θορύβου" +NoiseSuppress="Καταστολή θορύβου" Gain="Απολαβή" DelayMs="Καθυστέρηση (χιλιοστά του δευτερολέπτου)" Type="Τύπος" MaskBlendType.MaskColor="Μάσκα Άλφα (Κανάλι Χρώματος)" MaskBlendType.MaskAlpha="Μάσκα Άλφα (Κανάλι Άλφα)" +MaskBlendType.BlendMultiply="Ανάμιξη (πολλαπλασιάζονται)" +MaskBlendType.BlendAddition="Μείγμα (Προσθήκη)" +MaskBlendType.BlendSubtraction="Μείγμα (αφαίρεση)" Path="Διαδρομή" Color="Χρώμα" Opacity="Αδιαφάνεια" @@ -18,8 +28,11 @@ Brightness="Φωτεινότητα" Gamma="Γάμμα" BrowsePath.Images="Όλα τα αρχεία εικόνας" BrowsePath.AllFiles="Όλα τα αρχεία" +KeyColorType="Βασικό χρώμα τύπου" +KeyColor="Βασικό χρώμα" Similarity="Ομοιότητα (1-1000)" Smoothness="Ομαλότητα (1-1000)" +ColorSpillReduction="Βασικό χρώμα πετρελαιοκηλίδα μείωση (1-1000)" Crop.Left="Αριστερά" Crop.Right="Δεξιά" Crop.Top="Πάνω" @@ -36,15 +49,29 @@ Red="Κόκκινο" Green="Πράσινο" Blue="Μπλε" Magenta="Ματζέντα" +NoiseGate.OpenThreshold="Ανοικτό όριο (dB)" +NoiseGate.CloseThreshold="Κλείστό όριο (dB)" NoiseGate.AttackTime="Χρόνος προσβολής (msec)" NoiseGate.HoldTime="Χρόνος αναμονής (msec)" NoiseGate.ReleaseTime="Χρόνος διάχυσης (msec)" Gain.GainDB="Απολαβή (dB)" +StretchImage="Τέντωση εικόνας (απόρριψη αναλογίας εικόνας)" Resolution="Ανάλυση" None="Καμία" +ScaleFiltering="Κλίμακα φιλτραρίσματος" +ScaleFiltering.Point="Σημείο" +ScaleFiltering.Bilinear="Διγραμμικές" +ScaleFiltering.Bicubic="Δικυβική" +ScaleFiltering.Lanczos="Lanczos" +NoiseSuppress.SuppressLevel="Καταστολή επιπέδου (dB)" Saturation="Κορεσμός" HueShift="Μετατόπιση Απόχρωσης" Amount="Ποσό" +Compressor="Συμπιεστής" Compressor.Ratio="Αναλογία (X:1)" Compressor.Threshold="Κατώφλι (dB)" +Compressor.AttackTime="Επίθεση (ms)" +Compressor.ReleaseTime="Απελευθέρωση (ms)" +Compressor.OutputGain="Εξόδου κέρδος (dB)" +Compressor.SidechainSource="Πηγή sidechain/βουτιά" diff --git a/plugins/obs-filters/data/locale/gd-GB.ini b/plugins/obs-filters/data/locale/gd-GB.ini new file mode 100644 index 0000000..2f94ad5 --- /dev/null +++ b/plugins/obs-filters/data/locale/gd-GB.ini @@ -0,0 +1,26 @@ +ScrollFilter="Sgrolaich" +SharpnessFilter="Geuraich" +Type="Seòrsa" +Path="Slighe" +Color="Dath" +Contrast="Iomsgaradh" +Brightness="Soilleireachd" +BrowsePath.AllFiles="A h-uile faidhle" +Crop.Left="Clì" +Crop.Right="Deas" +Crop.Top="Barr" +Crop.Bottom="Bonn" +Crop.Width="Leud" +Crop.Height="Àirde" +Red="Dearg" +Green="Uaine" +Blue="Gorm" +Resolution="Dùmhlachd-bhreacaidh" +None="Chan eil gin" +ScaleFiltering="Criathradh sgèilidh" +ScaleFiltering.Point="Puing" +ScaleFiltering.Bilinear="Dà-loidhneach" +ScaleFiltering.Bicubic="Dà-chiùbach" +ScaleFiltering.Lanczos="Lanczos" +Saturation="Sàthachd" + diff --git a/plugins/obs-filters/data/locale/ka-GE.ini b/plugins/obs-filters/data/locale/ka-GE.ini index 9b0122a..7547556 100644 --- a/plugins/obs-filters/data/locale/ka-GE.ini +++ b/plugins/obs-filters/data/locale/ka-GE.ini @@ -1,4 +1,77 @@ +ColorFilter="ფერთა გასწორება" +ColorGradeFilter="LUT-ის ასახვა" +MaskFilter="სურათის ნიღაბი/შერევა" +AsyncDelayFilter="ვიდეოს დაყოვნება (ასინქრონული)" +CropFilter="შემოჭრა/არეები" +ScrollFilter="გადაადგილება" +ChromaKeyFilter="ფონის ჩანაცვლება (Chroma Key)" +ColorKeyFilter="ფერის ჩანაცვლება (Color Key)" +SharpnessFilter="სიმკვეთრის მომატება" +ScaleFilter="ზომების ცვლილება/გვერდების თანაფარდობა" +GPUDelayFilter="დაყოვნება დამუშავებისას" +UndistortCenter="ზეფართო სურათის შუაგულის გამრუდების არიდება, ზომების შეცვლისას" +NoiseGate="ხმაურის შეზღუდვა" +NoiseSuppress="ხმაურის დახშობა" +Gain="სიგნალის გაძლიერება" +DelayMs="დაყოვნება (მილიწამი)" +Type="სახეობა" +MaskBlendType.MaskColor="ალფა-ნიღაბი (ფერის არხი)" +MaskBlendType.MaskAlpha="ალფა-ნიღაბი (გამჭვირვალობის არხი)" +MaskBlendType.BlendMultiply="შერევა (გამრავლება)" +MaskBlendType.BlendAddition="შერევა (დამატება)" +MaskBlendType.BlendSubtraction="შერევა (გამოკლება)" +Path="მისამართი" +Color="ფერი" +Opacity="გაუმჭვირვალობა" +Contrast="კონტრასტი" +Brightness="სიკაშკაშე" +Gamma="ფერთა გამა" +BrowsePath.Images="ყველანაირი სურათი" +BrowsePath.AllFiles="ყველა ფაილი" +KeyColorType="საკვანძო ფერის სახე" +KeyColor="საკვანძო ფერი" +Similarity="მსგავსება (1-1000)" +Smoothness="სიგლუვე (1-1000)" +ColorSpillReduction="საკვანძო ფერთა გაბნევის შემცირება (1-1000)" +Crop.Left="მარცხენა" +Crop.Right="მარჯვენა" +Crop.Top="ზედა" +Crop.Bottom="ქვედა" +Crop.Width="სიგანე" +Crop.Height="სიმაღლე" +Crop.Relative="თანაფარდობა" +ScrollFilter.SpeedX="თარაზული სიჩქარე" +ScrollFilter.SpeedY="შვეული სიჩქარე" +ScrollFilter.LimitWidth="სიგანის შეზღუდვა" +ScrollFilter.LimitHeight="სიმაღლის შეზღუდვა" +CustomColor="მითითებული ფერი" Red="წითელი" Green="მწვანე" Blue="ლურჯი" +Magenta="მეწამული" +NoiseGate.OpenThreshold="ქვედა ზღურბლი (dB)" +NoiseGate.CloseThreshold="ზედა ზღურბლი (dB)" +NoiseGate.AttackTime="მომატების (Attack) ხანგრძლივობა (მილიწამი)" +NoiseGate.HoldTime="დაყოვნების (Hold) ხანგრძლივობა (მილიწამი)" +NoiseGate.ReleaseTime="შემცირების (Release) ხანგრძლივობა (მილიწამი)" +Gain.GainDB="გაძლიერება (dB)" +StretchImage="სურათის გაწელვა (გვერდების თანაფარდობის უგულებელყოფა)" +Resolution="გაფართოება" +None="არცერთი" +ScaleFiltering="მასშტაბირების ფილტრი" +ScaleFiltering.Point="წერტილოვანი" +ScaleFiltering.Bilinear="ორხაზოვანი" +ScaleFiltering.Bicubic="ბიკუბური" +ScaleFiltering.Lanczos="Lanczos" +NoiseSuppress.SuppressLevel="დახშობის ხარისხი (dB)" +Saturation="გაჯერებულობა" +HueShift="შეფერილობის შეცვლა" +Amount="რაოდენობა" +Compressor="დამხშობი" +Compressor.Ratio="ფარდობა (X:1)" +Compressor.Threshold="ზღურბლი (dB)" +Compressor.AttackTime="მომატება (მწ)" +Compressor.ReleaseTime="შემცირება (მწ)" +Compressor.OutputGain="გამომავალი სიგნალის გაძლიერება (dB)" +Compressor.SidechainSource="Sidechain/ხმის დონის დადაბლების წყარო" diff --git a/plugins/obs-filters/data/locale/vi-VN.ini b/plugins/obs-filters/data/locale/vi-VN.ini index b4beb14..dd770dc 100644 --- a/plugins/obs-filters/data/locale/vi-VN.ini +++ b/plugins/obs-filters/data/locale/vi-VN.ini @@ -23,6 +23,7 @@ Red="Đỏ" Green="Xanh" Blue="Xanh nước biển" Magenta="Đỏ tươi" +Resolution="Độ phân giải" None="Không có" Saturation="Độ bão hoà" diff --git a/plugins/obs-filters/noise-gate-filter.c b/plugins/obs-filters/noise-gate-filter.c index 2203338..0993e3b 100644 --- a/plugins/obs-filters/noise-gate-filter.c +++ b/plugins/obs-filters/noise-gate-filter.c @@ -41,8 +41,8 @@ struct noise_gate_data { float held_time; }; -#define VOL_MIN -96.0f -#define VOL_MAX 0.0f +#define VOL_MIN -96.0 +#define VOL_MAX 0.0 static const char *noise_gate_name(void *unused) { @@ -155,8 +155,8 @@ static struct obs_audio_data *noise_gate_filter_audio(void *data, static void noise_gate_defaults(obs_data_t *s) { - obs_data_set_default_double(s, S_OPEN_THRESHOLD, -26.0f); - obs_data_set_default_double(s, S_CLOSE_THRESHOLD, -32.0f); + obs_data_set_default_double(s, S_OPEN_THRESHOLD, -26.0); + obs_data_set_default_double(s, S_CLOSE_THRESHOLD, -32.0); obs_data_set_default_int (s, S_ATTACK_TIME, 25); obs_data_set_default_int (s, S_HOLD_TIME, 200); obs_data_set_default_int (s, S_RELEASE_TIME, 150); @@ -167,9 +167,9 @@ static obs_properties_t *noise_gate_properties(void *data) obs_properties_t *ppts = obs_properties_create(); obs_properties_add_float_slider(ppts, S_CLOSE_THRESHOLD, - TEXT_CLOSE_THRESHOLD, VOL_MIN, VOL_MAX, 1.0f); + TEXT_CLOSE_THRESHOLD, VOL_MIN, VOL_MAX, 1.0); obs_properties_add_float_slider(ppts, S_OPEN_THRESHOLD, - TEXT_OPEN_THRESHOLD, VOL_MIN, VOL_MAX, 1.0f); + TEXT_OPEN_THRESHOLD, VOL_MIN, VOL_MAX, 1.0); obs_properties_add_int(ppts, S_ATTACK_TIME, TEXT_ATTACK_TIME, 0, 10000, 1); obs_properties_add_int(ppts, S_HOLD_TIME, TEXT_HOLD_TIME, diff --git a/plugins/obs-filters/scroll-filter.c b/plugins/obs-filters/scroll-filter.c index 2ad8793..9ff361f 100644 --- a/plugins/obs-filters/scroll-filter.c +++ b/plugins/obs-filters/scroll-filter.c @@ -121,10 +121,10 @@ static obs_properties_t *scroll_filter_properties(void *data) obs_properties_add_float_slider(props, "speed_x", obs_module_text("ScrollFilter.SpeedX"), - -500.0f, 500.0f, 1.0f); + -500.0, 500.0, 1.0); obs_properties_add_float_slider(props, "speed_y", obs_module_text("ScrollFilter.SpeedY"), - -500.0f, 500.0f, 1.0f); + -500.0, 500.0, 1.0); p = obs_properties_add_bool(props, "limit_cx", obs_module_text("ScrollFilter.LimitWidth")); diff --git a/plugins/obs-filters/sharpness-filter.c b/plugins/obs-filters/sharpness-filter.c index 0e7a795..5269614 100644 --- a/plugins/obs-filters/sharpness-filter.c +++ b/plugins/obs-filters/sharpness-filter.c @@ -101,7 +101,7 @@ static obs_properties_t *sharpness_properties(void *data) obs_properties_t *props = obs_properties_create(); obs_properties_add_float_slider(props, "sharpness", - "Sharpness", 0.0f, 1.0f, 0.01f); + "Sharpness", 0.0, 1.0, 0.01); UNUSED_PARAMETER(data); return props; diff --git a/plugins/obs-libfdk/data/locale/gd-GB.ini b/plugins/obs-libfdk/data/locale/gd-GB.ini new file mode 100644 index 0000000..042eced --- /dev/null +++ b/plugins/obs-libfdk/data/locale/gd-GB.ini @@ -0,0 +1,4 @@ +LibFDK="Inneal-còdachaidh libfdk AAC" +Bitrate="Reat bhiotaichean" +Afterburner="Cuir an comas Afterburner AAC" + diff --git a/plugins/obs-libfdk/data/locale/ka-GE.ini b/plugins/obs-libfdk/data/locale/ka-GE.ini new file mode 100644 index 0000000..fbff316 --- /dev/null +++ b/plugins/obs-libfdk/data/locale/ka-GE.ini @@ -0,0 +1,4 @@ +LibFDK="libfdk AAC დამშიფრავი" +Bitrate="ბიტური სიხშირე" +Afterburner="AAC Afterburner-ის ჩართვა" + diff --git a/plugins/obs-libfdk/data/locale/ur-PK.ini b/plugins/obs-libfdk/data/locale/ur-PK.ini index eb205a3..1655d2b 100644 --- a/plugins/obs-libfdk/data/locale/ur-PK.ini +++ b/plugins/obs-libfdk/data/locale/ur-PK.ini @@ -1,2 +1,4 @@ LibFDK="لابفدک AAC انکوڈر" +Bitrate="بٹ شرح" +Afterburner="بٹرا قابل اطلاق AAC بعدبورنر" diff --git a/plugins/obs-outputs/CMakeLists.txt b/plugins/obs-outputs/CMakeLists.txt index 1bde00a..0c373cd 100644 --- a/plugins/obs-outputs/CMakeLists.txt +++ b/plugins/obs-outputs/CMakeLists.txt @@ -1,20 +1,25 @@ project(obs-outputs) -option(USE_SSL "Enable rtmps support with OpenSSL" OFF) +set(WITH_RTMPS AUTO CACHE STRING "Enable RTMPS support with mbedTLS") +set_property(CACHE WITH_RTMPS PROPERTY STRINGS AUTO ON OFF) -if (USE_SSL) - find_package(SSL QUIET) +option(STATIC_MBEDTLS "Statically link mbedTLS into binary" OFF) + +if (WITH_RTMPS OR (WITH_RTMPS STREQUAL "AUTO")) + find_package(MbedTLS QUIET) find_package(ZLIB QUIET) endif() -if (SSL_FOUND AND ZLIB_FOUND) - add_definitions(-DCRYPTO -DUSE_OPENSSL) - include_directories(${SSL_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS}) +if (LIBMBEDTLS_FOUND AND ZLIB_FOUND) + add_definitions(-DCRYPTO -DUSE_MBEDTLS) + include_directories(${LIBMBEDTLS_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS}) else() - if (USE_SSL) - message(WARNING "SSL enabled by user, but OpenSSL was not found") + if(WITH_RTMPS STREQUAL "AUTO") + message(WARNING "mbedTLS was not found, RTMPS will be auto-disabled") + elseif (WITH_RTMPS) + message(FATAL_ERROR "RTMPS enabled by user, but mbedTLS was not found") endif() - unset(SSL_LIBRARIES) + unset(LIBMBEDTLS_LIBRARIES) unset(ZLIB_LIBRARIES) add_definitions(-DNO_CRYPTO) endif() @@ -84,6 +89,12 @@ if(WIN32) ws2_32 winmm Iphlpapi) + + if (WITH_RTMPS OR (WITH_RTMPS STREQUAL "AUTO")) + SET(obs-outputs_PLATFORM_DEPS + ${obs-outputs_PLATFORM_DEPS} + crypt32) + endif() endif() if(MSVC) @@ -92,6 +103,16 @@ if(MSVC) w32-pthreads) endif() +if(APPLE AND (WITH_RTMPS OR (WITH_RTMPS STREQUAL "AUTO"))) + find_library(FOUNDATION_FRAMEWORK Foundation) + find_library(SECURITY_FRAMEWORK Security) + + set(obs-outputs_PLATFORM_DEPS + ${obs-outputs_PLATFORM_DEPS} + ${FOUNDATION_FRAMEWORK} + ${SECURITY_FRAMEWORK}) +endif() + set(obs-outputs_librtmp_HEADERS librtmp/amf.h librtmp/bytes.h @@ -133,7 +154,7 @@ set(obs-outputs_SOURCES flv-output.c flv-mux.c net-if.c) - + add_library(obs-outputs MODULE ${ftl_SOURCES} ${ftl_HEADERS} @@ -143,7 +164,7 @@ add_library(obs-outputs MODULE ${obs-outputs_librtmp_HEADERS}) target_link_libraries(obs-outputs libobs - ${SSL_LIBRARIES} + ${LIBMBEDTLS_LIBRARIES} ${ZLIB_LIBRARIES} ${ftl_IMPORTS} ${obs-outputs_PLATFORM_DEPS}) diff --git a/plugins/obs-outputs/data/locale/ar-SA.ini b/plugins/obs-outputs/data/locale/ar-SA.ini index 81799e9..29c197c 100644 --- a/plugins/obs-outputs/data/locale/ar-SA.ini +++ b/plugins/obs-outputs/data/locale/ar-SA.ini @@ -2,5 +2,8 @@ RTMPStream="تيار RTMP" RTMPStream.DropThreshold="انخفاض البداية (مللي ثانية)" FLVOutput="اخراج الملف بصيغة FLV" FLVOutput.FilePath="مسار الملف" +Default="Default" +ConnectionTimedOut="انتهت مهلة الاتصال. تأكد من أن قمت بتكوين خدمة البث صالحة ولا جدار الحماية بحظر الاتصال." +ConnectionReset="The connection was reset by the peer. This usually indicates internet connection problems between you and the streaming service." diff --git a/plugins/obs-outputs/data/locale/ca-ES.ini b/plugins/obs-outputs/data/locale/ca-ES.ini index ba0c81a..1471d15 100644 --- a/plugins/obs-outputs/data/locale/ca-ES.ini +++ b/plugins/obs-outputs/data/locale/ca-ES.ini @@ -11,4 +11,5 @@ ConnectionReset="La connexió s'ha acabat. Normalment això indica que hi ha pro HostNotFound="Nom d'amfitrió no trobat. Assegureu-vos que hi hagi configurat un servidor de transmissió vàlid i que la seva connexió a Internet / DNS estiguin funcionant correctament." NoData="Nom d'amfitrió trobat, però no hi ha dades del tipus sol·licitat. Això pot passar si heu enllaçat a una adreça IPv6 i el seu servei de transmissió només té adreces IPv4 (veure configuració / avançada)." AddressNotAvailable="Direcció no disponible. Potser heu intentat enllaçar amb una adreça IP no vàlida (veure configuració / avançada)." +SSLCertVerifyFailed="El servidor RTMP ha enviat un certificat SSL no vàlid." diff --git a/plugins/obs-outputs/data/locale/cs-CZ.ini b/plugins/obs-outputs/data/locale/cs-CZ.ini index 3c56618..e48340d 100644 --- a/plugins/obs-outputs/data/locale/cs-CZ.ini +++ b/plugins/obs-outputs/data/locale/cs-CZ.ini @@ -11,4 +11,5 @@ ConnectionReset="Připojení bylo resetováno druhou stranou. Toto obvykle zname HostNotFound="Hostitel nebyl nalezen. Zkontrolujte, zda jste zadali správný vysílací server a že vaše připojení k internetu / DNS funguje jak má." NoData="Hostitel byl nalezen, ale žádná data požadovaného typu. Toto se může stát, pokud používáte IPv6 adresu, ale vaše vysílací služba podporuje pouze připojení přes svou IPv4 adresu (viz. Nastavení / Rozšířené)." AddressNotAvailable="Adresa není k dispozici. Možná jste se snažili použít chybnou IP adresu (viz. Nastavení / Rozšířené)." +SSLCertVerifyFailed="RTMP server odeslal neplatný SSL certifikát." diff --git a/plugins/obs-outputs/data/locale/da-DK.ini b/plugins/obs-outputs/data/locale/da-DK.ini index a3b9fe7..eb3f973 100644 --- a/plugins/obs-outputs/data/locale/da-DK.ini +++ b/plugins/obs-outputs/data/locale/da-DK.ini @@ -11,4 +11,5 @@ ConnectionReset="Forbindelsen blev afbrudt. Dette indikerer typisk et problem me HostNotFound="Værtsnavn ikke fundet. Tjek at du har angivet en gyldig streaming-server, og at din Internetforbindelse/DNS fungerer korrekt." NoData="Værtsnavn fundet, men ingen data af den ønskede type. Dette kan forekomme, hvis du har tildelt en IPv6-adresse, og din streaming-tjeneste kun benytter IPv4-adresser (se Indstillinger/Avanceret)." AddressNotAvailable="Adresse utilgængelig. Du kan have forsøgt at tildele en ugyldig IP-adresse (se Indstillinger/Avanceret)." +SSLCertVerifyFailed="RTMP-serveren har sendt et ugyldig SSL-certifikat." diff --git a/plugins/obs-outputs/data/locale/de-DE.ini b/plugins/obs-outputs/data/locale/de-DE.ini index dcdb824..031a6c0 100644 --- a/plugins/obs-outputs/data/locale/de-DE.ini +++ b/plugins/obs-outputs/data/locale/de-DE.ini @@ -11,4 +11,5 @@ ConnectionReset="Die Verbindung wurde durch Kommunikationspartner zurückgesetzt HostNotFound="Hostname nicht gefunden. Stellen Sie sicher, dass Sie einen gültigen Streaming-Server eingegeben haben und Ihre Internetverbindung / DNS korrekt arbeiten." NoData="Hostname gefunden, aber keine Daten des angeforderten Typs. Dies kann auftreten, wenn Sie eine IPv6-Adresse verwenden und Ihr Streaming-Dienst nur über IPv4-Adressen verfügt (siehe Einstellungen / Erweitert)." AddressNotAvailable="Adresse nicht Verfügbar. Sie haben möglicherweise versucht, eine ungültige IP-Adresse zu verwenden (siehe Einstellungen / Erweitert)." +SSLCertVerifyFailed="Der RTMP-Server hat ein ungültiges SSL-Zertifikat gesendet." diff --git a/plugins/obs-outputs/data/locale/el-GR.ini b/plugins/obs-outputs/data/locale/el-GR.ini index 9d7dfce..3b8cb12 100644 --- a/plugins/obs-outputs/data/locale/el-GR.ini +++ b/plugins/obs-outputs/data/locale/el-GR.ini @@ -4,4 +4,11 @@ FLVOutput="FLV Αρχείο Εξόδου" FLVOutput.FilePath="Διαδρομή Αρχείου" Default="Προεπιλογή" +ConnectionTimedOut="Εξαντλήθηκε το χρονικό όριο της σύνδεσης. Βεβαιωθείτε ότι έχετε ρυθμίσει μια έγκυρη υπηρεσία συνεχούς ροής και το τείχος προστασίας δεν εμποδίζει τη σύνδεση." +PermissionDenied="Αποκλείστηκε η σύνδεση. Ελέγξτε το τείχος προστασίας / ρυθμίσεις προστασίας από ιούς για να βεβαιωθείτε οτι στο OBS επιτρέπεται να έχει πλήρης πρόσβαση στο διαδίκτυο." +ConnectionAborted="Η σύνδεση ματαιώθηκε. Αυτό συνήθως υποδεικνύει προβλήματα σύνδεσης στο διαδίκτυο ανάμεσα σε εσάς και την υπηρεσία συνεχούς ροής." +ConnectionReset="Η σύνδεση ήταν επαναφέρθηκε στον ομότιμο υπολογιστή. Αυτό συνήθως υποδεικνύει προβλήματα σύνδεσης στο διαδίκτυο ανάμεσα σε εσάς και την υπηρεσία συνεχούς ροής." +HostNotFound="Το όνομα του κεντρικού υπολογιστή δεν βρέθηκε. Βεβαιωθείτε ότι πληκτρολογήσατε έναν έγκυρο διακομιστή συνεχούς ροής και η σύνδεση στο διαδίκτυο / DNS λειτουργεί σωστά." +NoData="Το όνομα του κεντρικού υπολογιστή βρέθηκε, αλλά χωρίς δεδομένα του ζητούμενου τύπου. Αυτό μπορεί να συμβεί αν έχετε δεσμεύσει μια διεύθυνση IPv6 και για την υπηρεσία συνεχούς ροής μόνο διευθύνσεις IPv4 (ανατρέξτε στην ενότητα ρυθμίσεις)." +AddressNotAvailable="Η διεύθυνση δεν είναι διαθέσιμη. Μπορεί να έχετε δοκιμάσει να συνδεθείτε σε μια άκυρη διεύθυνση IP (βλ. ρυθμίσεις)." diff --git a/plugins/obs-outputs/data/locale/en-US.ini b/plugins/obs-outputs/data/locale/en-US.ini index 58596b1..3a915ce 100644 --- a/plugins/obs-outputs/data/locale/en-US.ini +++ b/plugins/obs-outputs/data/locale/en-US.ini @@ -11,3 +11,4 @@ ConnectionReset="The connection was reset by the peer. This usually indicates in HostNotFound="Hostname not found. Make sure you entered a valid streaming server and your internet connection / DNS are working correctly." NoData="Hostname found, but no data of the requested type. This can occur if you have bound to an IPv6 address and your streaming service only has IPv4 addresses (see Settings / Advanced)." AddressNotAvailable="Address not available. You may have tried to bind to an invalid IP address (see Settings / Advanced)." +SSLCertVerifyFailed="The RTMP server sent an invalid SSL certificate." diff --git a/plugins/obs-outputs/data/locale/es-ES.ini b/plugins/obs-outputs/data/locale/es-ES.ini index 8f4e0e1..80a91bb 100644 --- a/plugins/obs-outputs/data/locale/es-ES.ini +++ b/plugins/obs-outputs/data/locale/es-ES.ini @@ -11,4 +11,5 @@ ConnectionReset="La conexión se ha terminado. Normalmente esto indica que hay p HostNotFound="Nombre de host no encontrado. Asegúrese que haya configurado un servidor de transmisión valido y que su conexión a Internet / DNS estén funcionando correctamente." NoData="Nombre de host encontrado, pero no hay datos del tipo solicitado. Esto puede ocurrir si has enlazado a una dirección IPv6 y su servicio de streaming sólo tiene direcciones IPv4 (ver Configuración / Avanzada)." AddressNotAvailable="Dirección no disponible. Puede que hayas intentado enlazar con una dirección IP no valida (vea Configuración / Avanzado)." +SSLCertVerifyFailed="El servidor RTMP envió un certificado SSL no válido." diff --git a/plugins/obs-outputs/data/locale/eu-ES.ini b/plugins/obs-outputs/data/locale/eu-ES.ini index 663d34b..8e38430 100644 --- a/plugins/obs-outputs/data/locale/eu-ES.ini +++ b/plugins/obs-outputs/data/locale/eu-ES.ini @@ -11,4 +11,5 @@ ConnectionReset="Pareak berrezarri du konexioa. Transmisioaren zerbitzurekin kon HostNotFound="Ez da ostalari-izena topatu. Egiaztatu baliozko transmisio zerbitzaria jarri duzula eta zure Internet-konexioa eta DNSa zuzen ari direla lanean." NoData="Ostalari-izena topatu da baina eskatutako datu motatik batere ez. Gerta daiteke IPv6 helbide bat eskatu izana eta zure transmisio zerbitzuak bakarrik onartzea IPv4 helbideak (Ikus ezarpen aurreratuak)." AddressNotAvailable="Helbidea ez dago eskuragarri. Agian saiatu zara baliozkoa ez den IP helbide batera konektatzen (ikus ezarpen aurreratuak)." +SSLCertVerifyFailed="RTMP zerbitzariak baliorik gabeko SSL ziurtagiria bidali du." diff --git a/plugins/obs-outputs/data/locale/fi-FI.ini b/plugins/obs-outputs/data/locale/fi-FI.ini index 7a815c0..bed1ff4 100644 --- a/plugins/obs-outputs/data/locale/fi-FI.ini +++ b/plugins/obs-outputs/data/locale/fi-FI.ini @@ -11,4 +11,5 @@ ConnectionReset="Yhteys katkaistiin. Tämä tarkoittaa yleensä yhteysongelmia s HostNotFound="Isäntänimeä ei löytynyt. Varmista että syötit voimassaolevan lähetyspalvelimen ja että internet-yhteytesi tai DNS-palvelimesi toimivat oikein." NoData="Isäntänimi löytyi, mutta ei oikeanlaista pyydettyä dataa. Näin voi tapahtua jos olet rajannut yhteytesi IPv6 -osoitteeseen ja lähetyspalvelusi tukee vain IPv4-osoitteita (Katso Asetukset / Lisäasetukset)." AddressNotAvailable="Osoite ei ole saatavilla. Voi olla että yritit kiinnittää väärän IP-osoitteen (Katso Asetukset / Lisäasetukset)." +SSLCertVerifyFailed="RTMP-palvelin lähetti virheellisen SSL-sertifikaatin." diff --git a/plugins/obs-outputs/data/locale/fr-FR.ini b/plugins/obs-outputs/data/locale/fr-FR.ini index d326fdd..454150f 100644 --- a/plugins/obs-outputs/data/locale/fr-FR.ini +++ b/plugins/obs-outputs/data/locale/fr-FR.ini @@ -11,4 +11,5 @@ ConnectionReset="La connexion à été interrompue. Cela indique généralement HostNotFound="Nom d’hôte non trouvé. Assurez-vous que vous avez spécifié un serveur de diffusion valide et que votre connexion internet / DNS fonctionnent correctement." NoData="Nom d’hôte trouvé, mais aucune donnée du type requis. Cela peut se produire si vous avez lié à une adresse IPv6 et votre service de diffusion ne possède que des adresses IPv4 (voir Paramètres / Avancé)." AddressNotAvailable="Adresse non disponible. Vous avez peut-être essayé de la lier à une adresse IP non valide (voir Paramètres / Avancé)." +SSLCertVerifyFailed="Le serveur RTMP a fourni un certificat SSL incorrect." diff --git a/plugins/obs-outputs/data/locale/gd-GB.ini b/plugins/obs-outputs/data/locale/gd-GB.ini new file mode 100644 index 0000000..2c1f2f6 --- /dev/null +++ b/plugins/obs-outputs/data/locale/gd-GB.ini @@ -0,0 +1,7 @@ +RTMPStream="Sruthadh RTMP" +RTMPStream.DropThreshold="Stairsneach an tuiteim (mille-dhiog)" +FLVOutput="Às-chur faidhle FLV" +FLVOutput.FilePath="Slighe an fhaidhle" +Default="Bun-roghainn" + + diff --git a/plugins/obs-outputs/data/locale/hu-HU.ini b/plugins/obs-outputs/data/locale/hu-HU.ini index 41352d9..631f19f 100644 --- a/plugins/obs-outputs/data/locale/hu-HU.ini +++ b/plugins/obs-outputs/data/locale/hu-HU.ini @@ -11,4 +11,5 @@ ConnectionReset="A kapcsolat a peer által megszakítva. Ez általában azt jelz HostNotFound="A hostnév nem található. Győződjön meg róla, hogy érvényes stream szervert adott meg és az internetkapcsolata / DNS szerver megfelelően működik." NoData="Hostnév megtalálva, viszont a kért típusú állomány nem elérhető. Ez akkor fordul elő, ha IPv6 címhez van rendelve és a stream kiszolgálójának csak IPv4 címei állnak rendelkezésre (lásd: Beállítások / Haladó)." AddressNotAvailable="A cím nem elérhető. Valószínűleg egy érvénytelen IP címet adott meg (Lásd: Beállítások / Haladó)." +SSLCertVerifyFailed="Az RTMP kiszolgáló által küldött SSL tanúsítvány érvénytelen." diff --git a/plugins/obs-outputs/data/locale/ja-JP.ini b/plugins/obs-outputs/data/locale/ja-JP.ini index 25f6a00..0366cfb 100644 --- a/plugins/obs-outputs/data/locale/ja-JP.ini +++ b/plugins/obs-outputs/data/locale/ja-JP.ini @@ -11,4 +11,5 @@ ConnectionReset="接続はピアによってリセットされました。 ス HostNotFound="ホスト名が見つかりません。 有効なストリーミングサーバーを入力していることとインターネット接続/DNSが正しく機能していることを確認してください。" NoData="ホスト名が見つかりましたが、要求されたタイプのデータがありません。 これはIPv6アドレスにバインドしている状態でストリーミングサービスにIPv4アドレスしかない場合に発生します。 (設定 / 詳細設定 を参照)" AddressNotAvailable="アドレスを利用できません。 無効なIPアドレスにバインドしようとした可能性があります。 (設定 / 詳細設定 を参照)" +SSLCertVerifyFailed="RTMPサーバーが無効なSSL証明書を送信しました。" diff --git a/plugins/obs-outputs/data/locale/ka-GE.ini b/plugins/obs-outputs/data/locale/ka-GE.ini new file mode 100644 index 0000000..933c06b --- /dev/null +++ b/plugins/obs-outputs/data/locale/ka-GE.ini @@ -0,0 +1,15 @@ +RTMPStream="RTMP ნაკადი" +RTMPStream.DropThreshold="ქვედა ზღურბლი (მილიწამი)" +FLVOutput="გამომავალი FLV ფაილი" +FLVOutput.FilePath="ფაილის მისამართი" +Default="ნაგულისხმევი" + +ConnectionTimedOut="კავშირის ვადა ამოიწურა. გადაამოწმეთ, სწორად გაქვთ თუ არა გამართული ნაკადის გაშვების მომსახურება და ქსელის ფარი ხომ არ ზღუდავს კავშირს." +PermissionDenied="კავშირი შეიზღუდა. გადაამოწმეთ ქსელის ფარის ან ანტივირუსული პროგრამის პარამეტრები და დარწმუნდით, რომ OBS-ს აქვს სრული დაშვება ინტერნეტთან." +ConnectionAborted="კავშირი გაუქმდა. ძირითადად, ეს მიუთითებს ინტერნეტკავშირის ხარვეზების არსებობას, თქვენსა და ნაკადის გაშვების მომსახურების მომწოდებელს შორის." +ConnectionReset="კავშირი გაწყდა ერთ-ერთი მხარის მიერ. ძირითადად, ეს მიუთითებს ინტერნეტკავშირის ხარვეზების არსებობას, თქვენსა და ნაკადის გაშვების მომსახურების მომწოდებელს შორის." +HostNotFound="დაკავშირების წერტილი ვერ მოიძებნა. დარწმუნდით, რომ სწორად უთითებთ ნაკადის გაშვების მომსახურების მონაცემებს და თქვენი DNS / ინტერნეტკავშირის პარამეტრებიც სწორადაა გამართული." +NoData="დაკავშირების წერტილი მოიძებნა, მაგრამ მოთხოვნილი სახის მონაცემები არა. ეს შეიძლება გამოწვეული იყოს იმით, რომ თქვენ უკავშირდებით IPv6 მისამართზე, ხოლო თქვენს ნაკადის გაშვების მომსახურებას, მხოლოდ IPv4 მისამართები გააჩნია (იხილეთ პარამეტრები / დამატებითი)." +AddressNotAvailable="მისამართი მიუწვდომელია. შესაძლოა, თქვენ ცდილობთ მცდარ IP მისამართზე დაკავშირებას (იხილეთ პარამეტრები / დამატებითი)." +SSLCertVerifyFailed="RTMP სერვერმა გაგზავნა არამართებული SSL სერტიფიკატი." + diff --git a/plugins/obs-outputs/data/locale/ko-KR.ini b/plugins/obs-outputs/data/locale/ko-KR.ini index 6ec434f..f3c114f 100644 --- a/plugins/obs-outputs/data/locale/ko-KR.ini +++ b/plugins/obs-outputs/data/locale/ko-KR.ini @@ -11,4 +11,5 @@ ConnectionReset="상호 연결 문제로 초기화되었습니다. 보통 사용 HostNotFound="호스트 이름을 찾을 수 없습니다. 방송 서버 정보가 제대로 입력되었는지 확인하고, 인터넷 접속 혹은 DNS가 제대로 작동하고 있는지 점검하십시오." NoData="호스트 이름은 찾았지만 요청한 형식의 데이터가 없습니다. 이 문제는 보통 사용자가 IPv6 형식의 주소를 고정하여 사용하면서 IPv4 형식의 주소만 지원하는 방송 서비스에 접속을 시도한 경우 나타납니다 (설정 / 고급 창을 확인하십시오)." AddressNotAvailable="주소를 사용할 수 없습니다. 잘못된 IP주소를 고정하고 있습니다 (설정 / 고급 창을 확인하십시오)." +SSLCertVerifyFailed="해당 RTMP 서버는 잘못된 SSL 인증서를 보냈습니다." diff --git a/plugins/obs-outputs/data/locale/nb-NO.ini b/plugins/obs-outputs/data/locale/nb-NO.ini index 555a665..7be490c 100644 --- a/plugins/obs-outputs/data/locale/nb-NO.ini +++ b/plugins/obs-outputs/data/locale/nb-NO.ini @@ -11,4 +11,5 @@ ConnectionReset="Tilkoblingen ble avbrutt. Dette betyr vanligvis at det er probl HostNotFound="Tjeneren ble ikke funnet. Kontroller at du har angitt en gyldig streaming server og at tilkoblingen / DNS fungerer." NoData="Tjeneren funnet, men ingen data for den forespurte typen. Dette kan skje hvis du har bundet til en IPv6-adresse og streaming tjeneste har bare IPv4-adresser (se Snnstillinger / Avansert)." AddressNotAvailable="Adresse ikke tilgjengelig. Du prøvde å binde til en ugyldig IP-adresse (se Innstillinger / Avansert)." +SSLCertVerifyFailed="RTMP-tjeneren sendte et ugyldig SSL-sertifikat." diff --git a/plugins/obs-outputs/data/locale/nl-NL.ini b/plugins/obs-outputs/data/locale/nl-NL.ini index 0ef923f..e72f7a1 100644 --- a/plugins/obs-outputs/data/locale/nl-NL.ini +++ b/plugins/obs-outputs/data/locale/nl-NL.ini @@ -11,4 +11,5 @@ ConnectionReset="De verbinding was gereset door de andere partij. Dit duidt mees HostNotFound="Hostname niet gevonden. Controleer dat je een geldige streaming service hebt ingevuld en dat je internetverbinding / DNS correct werken." NoData="Hostname gevonden, maar geen data van het verwachte type. Dit kan gebeuren als je aan een IPv6 adres hebt gebonden, en je streaming service alleen IPv4 adressen heeft (zie Instellingen / Geavanceerd)." AddressNotAvailable="Adres niet beschikbaar. Je hebt misschien geprobeerd om aan een ongeldig IP adres te binden (zie Instellingen / Geavanceerd)." +SSLCertVerifyFailed="De RTMP-server heeft een ongeldig SSL-certificaat verzonden." diff --git a/plugins/obs-outputs/data/locale/pl-PL.ini b/plugins/obs-outputs/data/locale/pl-PL.ini index f612ef3..735c3c3 100644 --- a/plugins/obs-outputs/data/locale/pl-PL.ini +++ b/plugins/obs-outputs/data/locale/pl-PL.ini @@ -11,4 +11,5 @@ ConnectionReset="Połączenie zostało przerwane po stronie serwera. Wskazuje to HostNotFound="Nie znaleziono nazwy hosta. Upewnij się, że wprowadzono prawidłowe dane serwera przesyłania strumieniowego i połączenie z internetem / DNS są poprawne." NoData="Nazwa serwera została znaleziona ale nie stwierdzono poprawności odbieranych danych. Dzieje się tak najczęściej po przypisaniu aplikacji do adresu IPv6, gdy usługa strumieniowania obsługuje jedynie adresy IPv4 (zobacz Ustawienia -> Zaawansowane)." AddressNotAvailable="Adres IP niedostępny. Być może powiązano aplikację z nieprawidłowym adresem IP (zobacz Ustawienia -> Zaawansowane)." +SSLCertVerifyFailed="Serwer RTMP wysłał nieprawidłowy certyfikat SSL." diff --git a/plugins/obs-outputs/data/locale/pt-BR.ini b/plugins/obs-outputs/data/locale/pt-BR.ini index 3ede596..7cd9b39 100644 --- a/plugins/obs-outputs/data/locale/pt-BR.ini +++ b/plugins/obs-outputs/data/locale/pt-BR.ini @@ -11,4 +11,5 @@ ConnectionReset="A conexão foi redefinida pelo usuário. Isso geralmente indica HostNotFound="Host não encontrado. Verifique se você inseriu um servidor válido de transmissão e se sua conexão de internet / DNS estão funcionando corretamente." NoData="Host encontrado, mas não há dados do tipo solicitado. Isso pode ocorrer se você tiver vinculado a um endereço IPv6 e seu serviço de transmissão tem apenas endereços IPv4 (consulte Configurações / Avançado)." AddressNotAvailable="Endereço não disponível. Você pode ter tentado se vincular a um endereço IP inválido (consulte Configurações / Avançado)." +SSLCertVerifyFailed="O servidor RTMP enviou um certificado SSL inválido." diff --git a/plugins/obs-outputs/data/locale/ru-RU.ini b/plugins/obs-outputs/data/locale/ru-RU.ini index 7cf4f6f..3834a25 100644 --- a/plugins/obs-outputs/data/locale/ru-RU.ini +++ b/plugins/obs-outputs/data/locale/ru-RU.ini @@ -11,4 +11,5 @@ ConnectionReset="Соединение было сброшено одноранг HostNotFound="Имя узла не найдено. Убедитесь, что вы ввели действительный сервер вещания и ваше подключение к интернету/DNS работают правильно." NoData="Имя узла найдено, но нет данных запрошенного типа. Такое может случиться, если вы привязаны к IPv6-адресу, а ваш сервис вещания имеет только IPv4-адреса (смотрите Настройки - Расширенные)." AddressNotAvailable="Адрес недоступен. Возможно вы пытались привязаться к недействительному IP-адресу (смотрите Настройки - Расширенные)." +SSLCertVerifyFailed="RTMP сервер отправил недействительный сертификат SSL." diff --git a/plugins/obs-outputs/data/locale/sv-SE.ini b/plugins/obs-outputs/data/locale/sv-SE.ini index f8b8d45..f124ace 100644 --- a/plugins/obs-outputs/data/locale/sv-SE.ini +++ b/plugins/obs-outputs/data/locale/sv-SE.ini @@ -11,4 +11,5 @@ ConnectionReset="Anslutningen återställdes av en peer. Detta kan indikera prob HostNotFound="Värdnamnet hittades inte. Se till att du har angivit en giltigt strömningstjänst och att din Internetanslutning / DNS fungerar på rätt sätt." NoData="Värdnamnet hittades, men ingen data av den begärda typen. Detta kan hända om du ansluter till en IPv6-adress och din strömningstjänst endast har IPv4-adresser (gå till Inställningar / Avancerat)." AddressNotAvailable="Adressen är inte tillgänglig. Du kanske försökte ansluta till en ogiltig IP-adress (gå till Inställningar / Avancerat)." +SSLCertVerifyFailed="RTMP-servern skickade ett ogiltigt SSL-certifikat." diff --git a/plugins/obs-outputs/data/locale/tr-TR.ini b/plugins/obs-outputs/data/locale/tr-TR.ini index 8215642..61aa6c6 100644 --- a/plugins/obs-outputs/data/locale/tr-TR.ini +++ b/plugins/obs-outputs/data/locale/tr-TR.ini @@ -11,4 +11,5 @@ ConnectionReset="Bağlantı karşı taraftan sıfırlandı. Bu genellikle sizin HostNotFound="Ana bilgisayar adı bulunamadı. Geçerli bir yayın sunucusu girdiğinizden ve internet bağlantınızın / DNS'nizin düzgün çalıştığını emin olun." NoData="Ana bilgisayar adı bulundu, ancak istenen türde veri bulunamadı. Bu bir IPv6 adresine bağlamış ve yayın servisinizin sadece IPv4 adresleri varsa oluşabilir (bkz: Ayarlar / Gelişmiş)." AddressNotAvailable="Adres kullanılamaz. Geçersiz bir IP adresi bağlamayı denemiş olabilirsiniz (bakın: Ayarlar / Gelişmiş)." +SSLCertVerifyFailed="RTMP sunucusu geçersiz bir SSL sertifikası gönderdi." diff --git a/plugins/obs-outputs/data/locale/uk-UA.ini b/plugins/obs-outputs/data/locale/uk-UA.ini index aa253b3..6ab98ef 100644 --- a/plugins/obs-outputs/data/locale/uk-UA.ini +++ b/plugins/obs-outputs/data/locale/uk-UA.ini @@ -11,4 +11,5 @@ ConnectionReset="З'єднання було скинуте рівноправн HostNotFound="Ім'я хоста, не знайдено. Переконайтеся, що ви ввели дійсний сервер трансляцій і підключення до Інтернету / DNS працює правильно." NoData="Ім'я хоста знайдено, але нема жодних даних вказаного типу. Це може статися, якщо ви вказали прив'язку до IPv6-адресу, але ваш сервіс трансляцій підтримує лише адреси IPv4 (див. Налаштування / Розширені)." AddressNotAvailable="Адреса недоступна. Напевно ви спробували прив'язатись до адаптера з неіснуючую IP-адресою (див. Налаштування / Розширені)." +SSLCertVerifyFailed="RTMP сервер надіслав неприпустимий сертифікат SSL." diff --git a/plugins/obs-outputs/data/locale/zh-CN.ini b/plugins/obs-outputs/data/locale/zh-CN.ini index 950273d..e8a99af 100644 --- a/plugins/obs-outputs/data/locale/zh-CN.ini +++ b/plugins/obs-outputs/data/locale/zh-CN.ini @@ -11,4 +11,5 @@ ConnectionReset="对方重置连接. 这通常表明你和流媒体服务之间 HostNotFound="找不到 Hostname. 请确保您输入一个有效的流媒体服务器并且您的互联网连接 / DNS 工作正常." NoData="Hostname 发现, 但没有请求的类型的数据的主机名. 这有可能因为你绑定到 IPv6 地址并且你的流媒体服务仅有 IPv4 地址 (请参阅设置 / 高级)." AddressNotAvailable="没有可用的地址. 你可能在试图绑定到一个无效的 IP 地址 (请参阅设置 / 高级)." +SSLCertVerifyFailed="RTMP 服务器发送了无效的 SSL 证书。" diff --git a/plugins/obs-outputs/data/locale/zh-TW.ini b/plugins/obs-outputs/data/locale/zh-TW.ini index ab05bb8..15d5fc6 100644 --- a/plugins/obs-outputs/data/locale/zh-TW.ini +++ b/plugins/obs-outputs/data/locale/zh-TW.ini @@ -11,4 +11,5 @@ ConnectionReset="連線被對方重置。通常這代表您與串流服務之間 HostNotFound="找不到主機名稱。請確定輸入了一個有效的串流服務器且網路連線跟 DNS 工作正常。" NoData="找到主機名稱,但沒有要求類型的資料。這可能發生在您綁定於 IPv6 位址但串流服務只有 IPv4 位址 (請看 設定/進階)。" AddressNotAvailable="位址不可用。可能因為嘗試綁定到一個不正確 IP 位址(請確認 設定/進階 的設定)。" +SSLCertVerifyFailed="RTMP 伺服器發送了一則不合法的 SSL 憑證。" diff --git a/plugins/obs-outputs/flv-mux.c b/plugins/obs-outputs/flv-mux.c index 6c6c0eb..b1b2512 100644 --- a/plugins/obs-outputs/flv-mux.c +++ b/plugins/obs-outputs/flv-mux.c @@ -74,7 +74,7 @@ static bool build_flv_meta_data(obs_output_t *context, enc_str(&enc, end, "onMetaData"); *enc++ = AMF_ECMA_ARRAY; - enc = AMF_EncodeInt32(enc, end, a_idx == 0 ? 14 : 9); + enc = AMF_EncodeInt32(enc, end, a_idx == 0 ? 20 : 15); enc_num_val(&enc, end, "duration", 0.0); enc_num_val(&enc, end, "fileSize", 0.0); diff --git a/plugins/obs-outputs/librtmp/amf.c b/plugins/obs-outputs/librtmp/amf.c index ef44f0c..0821161 100644 --- a/plugins/obs-outputs/librtmp/amf.c +++ b/plugins/obs-outputs/librtmp/amf.c @@ -29,6 +29,7 @@ #include "bytes.h" static const AMFObjectProperty AMFProp_Invalid = { {0, 0}, AMF_INVALID }; +static const AMFObject AMFObj_Invalid = { 0, 0 }; static const AVal AV_empty = { 0, 0 }; /* Data is Big-Endian */ @@ -336,13 +337,19 @@ AMFProp_GetBoolean(AMFObjectProperty *prop) void AMFProp_GetString(AMFObjectProperty *prop, AVal *str) { - *str = prop->p_vu.p_aval; + if (prop->p_type == AMF_STRING) + *str = prop->p_vu.p_aval; + else + *str = AV_empty; } void AMFProp_GetObject(AMFObjectProperty *prop, AMFObject *obj) { - *obj = prop->p_vu.p_object; + if (prop->p_type == AMF_OBJECT) + *obj = prop->p_vu.p_object; + else + *obj = AMFObj_Invalid; } int @@ -472,6 +479,8 @@ AMF3ReadString(const char *data, AVal *str) RTMP_Log(RTMP_LOGDEBUG, "%s, string reference, index: %d, not supported, ignoring!", __FUNCTION__, refIndex); + str->av_val = NULL; + str->av_len = 0; return len; } else @@ -511,9 +520,12 @@ AMF3Prop_Decode(AMFObjectProperty *prop, const char *pBuffer, int nSize, if (name.av_len <= 0) return nRes; + nSize -= nRes; + if (nSize <= 0) + return -1; + prop->p_name = name; pBuffer += nRes; - nSize -= nRes; } /* decode */ @@ -601,6 +613,9 @@ AMF3Prop_Decode(AMFObjectProperty *prop, const char *pBuffer, int nSize, return -1; } + if (nSize < 0) + return -1; + return nOriginalSize - nSize; } @@ -994,9 +1009,18 @@ AMF_DecodeArray(AMFObject *obj, const char *pBuffer, int nSize, int nRes; nArrayLen--; + if (nSize <= 0) + { + bError = TRUE; + break; + } + nRes = AMFProp_Decode(&prop, pBuffer, nSize, bDecodeName); if (nRes == -1) + { bError = TRUE; + break; + } else { nSize -= nRes; @@ -1057,12 +1081,12 @@ AMF3_Decode(AMFObject *obj, const char *pBuffer, int nSize, int bAMFData) else { int32_t classExtRef = (classRef >> 1); - int i; + int i, cdnum; cd.cd_externalizable = (classExtRef & 0x1) == 1; cd.cd_dynamic = ((classExtRef >> 1) & 0x1) == 1; - cd.cd_num = classExtRef >> 2; + cdnum = classExtRef >> 2; /* class name */ @@ -1077,9 +1101,16 @@ AMF3_Decode(AMFObject *obj, const char *pBuffer, int nSize, int bAMFData) cd.cd_name.av_val, cd.cd_externalizable, cd.cd_dynamic, cd.cd_num); - for (i = 0; i < cd.cd_num; i++) + for (i = 0; i < cdnum; i++) { AVal memberName = AV_empty; + if (nSize <= 0) + { + invalid: + RTMP_Log(RTMP_LOGDEBUG, "%s, invalid class encoding!", + __FUNCTION__); + return nOriginalSize; + } len = AMF3ReadString(pBuffer, &memberName); RTMP_Log(RTMP_LOGDEBUG, "Member: %s", memberName.av_val); AMF3CD_AddProp(&cd, &memberName); @@ -1115,6 +1146,8 @@ AMF3_Decode(AMFObject *obj, const char *pBuffer, int nSize, int bAMFData) int nRes, i; for (i = 0; i < cd.cd_num; i++) /* non-dynamic */ { + if (nSize <= 0) + goto invalid; nRes = AMF3Prop_Decode(&prop, pBuffer, nSize, FALSE); if (nRes == -1) RTMP_Log(RTMP_LOGDEBUG, "%s, failed to decode AMF3 property!", @@ -1132,6 +1165,8 @@ AMF3_Decode(AMFObject *obj, const char *pBuffer, int nSize, int bAMFData) do { + if (nSize <= 0) + goto invalid; nRes = AMF3Prop_Decode(&prop, pBuffer, nSize, TRUE); AMF_AddProp(obj, &prop); @@ -1179,10 +1214,18 @@ AMF_Decode(AMFObject *obj, const char *pBuffer, int nSize, int bDecodeName) nRes = AMFProp_Decode(&prop, pBuffer, nSize, bDecodeName); if (nRes == -1) + { bError = TRUE; + break; + } else { nSize -= nRes; + if (nSize < 0) + { + bError = TRUE; + break; + } pBuffer += nRes; AMF_AddProp(obj, &prop); } diff --git a/plugins/obs-outputs/librtmp/dh.h b/plugins/obs-outputs/librtmp/dh.h index d43ef80..e32fe94 100644 --- a/plugins/obs-outputs/librtmp/dh.h +++ b/plugins/obs-outputs/librtmp/dh.h @@ -21,7 +21,59 @@ * http://www.gnu.org/copyleft/lgpl.html */ -#ifdef USE_POLARSSL +#if defined(USE_MBEDTLS) +#include +#include +typedef mbedtls_mpi* MP_t; +#define MP_new(m) m = malloc(sizeof(mbedtls_mpi)); mbedtls_mpi_init(m) +#define MP_set_w(mpi, w) mbedtls_mpi_lset(mpi, w) +#define MP_cmp(u, v) mbedtls_mpi_cmp_mpi(u, v) +#define MP_set(u, v) mbedtls_mpi_copy(u, v) +#define MP_sub_w(mpi, w) mbedtls_mpi_sub_int(mpi, mpi, w) +#define MP_cmp_1(mpi) mbedtls_mpi_cmp_int(mpi, 1) +#define MP_modexp(r, y, q, p) mbedtls_mpi_exp_mod(r, y, q, p, NULL) +#define MP_free(mpi) mbedtls_mpi_free(mpi); free(mpi) +#define MP_gethex(u, hex, res) MP_new(u); res = mbedtls_mpi_read_string(u, 16, hex) == 0 +#define MP_bytes(u) mbedtls_mpi_size(u) +#define MP_setbin(u,buf,len) mbedtls_mpi_write_binary(u,buf,len) +#define MP_getbin(u,buf,len) MP_new(u); mbedtls_mpi_read_binary(u,buf,len) + +typedef struct MDH +{ + MP_t p; + MP_t g; + MP_t pub_key; + MP_t priv_key; + long length; + mbedtls_dhm_context ctx; +} MDH; + +#define MDH_new() calloc(1,sizeof(MDH)) +#define MDH_free(vp) {MDH *_dh = vp; mbedtls_dhm_free(&_dh->ctx); MP_free(_dh->p); MP_free(_dh->g); MP_free(_dh->pub_key); MP_free(_dh->priv_key); free(_dh);} + +static int MDH_generate_key(MDH *dh) +{ + unsigned char out[2]; + MP_set(&dh->ctx.P, dh->p); + MP_set(&dh->ctx.G, dh->g); + dh->ctx.len = 128; + mbedtls_dhm_make_public(&dh->ctx, 1024, out, 1, mbedtls_ctr_drbg_random, &RTMP_TLS_ctx->ctr_drbg); + MP_new(dh->pub_key); + MP_new(dh->priv_key); + MP_set(dh->pub_key, &dh->ctx.GX); + MP_set(dh->priv_key, &dh->ctx.X); + return 1; +} + +static int MDH_compute_key(uint8_t *secret, size_t len, MP_t pub, MDH *dh) +{ + MP_set(&dh->ctx.GY, pub); + size_t olen; + mbedtls_dhm_calc_secret(&dh->ctx, secret, len, &olen, NULL, NULL); + return 0; +} + +#elif defined(USE_POLARSSL) #include typedef mpi * MP_t; #define MP_new(m) m = malloc(sizeof(mpi)); mpi_init(m) @@ -271,7 +323,7 @@ DHGetPublicKey(MDH *dh, uint8_t *pubkey, size_t nPubkeyLen) if (!dh || !dh->pub_key) return 0; - len = MP_bytes(dh->pub_key); + len = (int)MP_bytes(dh->pub_key); if (len <= 0 || len > (int) nPubkeyLen) return 0; diff --git a/plugins/obs-outputs/librtmp/handshake.h b/plugins/obs-outputs/librtmp/handshake.h index 1636261..8cd3e32 100644 --- a/plugins/obs-outputs/librtmp/handshake.h +++ b/plugins/obs-outputs/librtmp/handshake.h @@ -24,7 +24,28 @@ /* This file is #included in rtmp.c, it is not meant to be compiled alone */ -#ifdef USE_POLARSSL +#if defined(USE_MBEDTLS) +#include +#include +#ifndef SHA256_DIGEST_LENGTH +#define SHA256_DIGEST_LENGTH 32 +#endif +typedef mbedtls_md_context_t *HMAC_CTX; +#define HMAC_setup(ctx, key, len) ctx = malloc(sizeof(mbedtls_md_context_t)); mbedtls_md_init(ctx); \ + mbedtls_md_setup(ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1); \ + mbedtls_md_hmac_starts(ctx, (const unsigned char *)key, len) +#define HMAC_crunch(ctx, buf, len) mbedtls_md_hmac_update(ctx, buf, len) +#define HMAC_finish(ctx, dig, dlen) dlen = SHA256_DIGEST_LENGTH; mbedtls_md_hmac_finish(ctx, dig) +#define HMAC_close(ctx) mbedtls_md_free(ctx); free(ctx); ctx = NULL + +typedef mbedtls_arc4_context* RC4_handle; +#define RC4_alloc(h) *h = malloc(sizeof(mbedtls_arc4_context)); mbedtls_arc4_init(*h) +#define RC4_setkey(h,l,k) mbedtls_arc4_setup(h,k,l) +#define RC4_encrypt(h,l,d) mbedtls_arc4_crypt(h,l,(unsigned char *)d,(unsigned char *)d) +#define RC4_encrypt2(h,l,s,d) mbedtls_arc4_crypt(h,l,(unsigned char *)s,(unsigned char *)d) +#define RC4_free(h) mbedtls_arc4_free(h); free(h); h = NULL + +#elif defined(USE_POLARSSL) #include #include #ifndef SHA256_DIGEST_LENGTH @@ -148,6 +169,8 @@ typedef unsigned int (getoff)(uint8_t *buf, unsigned int len); static unsigned int GetDHOffset2(uint8_t *handshake, unsigned int len) { + (void) len; + unsigned int offset = 0; uint8_t *ptr = handshake + 768; unsigned int res; @@ -177,6 +200,8 @@ GetDHOffset2(uint8_t *handshake, unsigned int len) static unsigned int GetDigestOffset2(uint8_t *handshake, unsigned int len) { + (void) len; + unsigned int offset = 0; uint8_t *ptr = handshake + 772; unsigned int res; @@ -206,6 +231,8 @@ GetDigestOffset2(uint8_t *handshake, unsigned int len) static unsigned int GetDHOffset1(uint8_t *handshake, unsigned int len) { + (void) len; + unsigned int offset = 0; uint8_t *ptr = handshake + 1532; unsigned int res; @@ -235,6 +262,8 @@ GetDHOffset1(uint8_t *handshake, unsigned int len) static unsigned int GetDigestOffset1(uint8_t *handshake, unsigned int len) { + (void) len; + unsigned int offset = 0; uint8_t *ptr = handshake + 8; unsigned int res; @@ -1128,6 +1157,7 @@ HandShake(RTMP * r, int FP9HandShake) __FUNCTION__); } } + // TODO(mgoulet): Should this have a HMAC_finish here? RTMP_Log(RTMP_LOGDEBUG, "%s: Handshaking finished....", __FUNCTION__); return TRUE; @@ -1482,6 +1512,8 @@ SHandShake(RTMP * r) } } + // TODO(mgoulet): Should this have an Rc4_free? + RTMP_Log(RTMP_LOGDEBUG, "%s: Handshaking finished....", __FUNCTION__); return TRUE; } diff --git a/plugins/obs-outputs/librtmp/hashswf.c b/plugins/obs-outputs/librtmp/hashswf.c index b8d5a9e..b6c2b61 100644 --- a/plugins/obs-outputs/librtmp/hashswf.c +++ b/plugins/obs-outputs/librtmp/hashswf.c @@ -30,7 +30,20 @@ #pragma GCC diagnostic ignored "-Wdeprecated-declarations" #endif -#ifdef USE_POLARSSL +#if defined(USE_MBEDTLS) +#include +#ifndef SHA256_DIGEST_LENGTH +#define SHA256_DIGEST_LENGTH 32 +#endif +typedef mbedtls_md_context_t *HMAC_CTX; +#define HMAC_setup(ctx, key, len) ctx = malloc(sizeof(mbedtls_md_context_t)); mbedtls_md_init(ctx); \ + mbedtls_md_setup(ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1); \ + mbedtls_md_hmac_starts(ctx, (const unsigned char *)key, len) +#define HMAC_crunch(ctx, buf, len) mbedtls_md_hmac_update(ctx, buf, len) +#define HMAC_finish(ctx, dig, dlen) dlen = SHA256_DIGEST_LENGTH; mbedtls_md_hmac_finish(ctx, dig) +#define HMAC_close(ctx) free(ctx); mbedtls_md_free(ctx); ctx = NULL + +#elif defined(USE_POLARSSL) #include #ifndef SHA256_DIGEST_LENGTH #define SHA256_DIGEST_LENGTH 32 @@ -40,6 +53,7 @@ #define HMAC_crunch(ctx, buf, len) sha2_hmac_update(&ctx, buf, len) #define HMAC_finish(ctx, dig, dlen) dlen = SHA256_DIGEST_LENGTH; sha2_hmac_finish(&ctx, dig) #define HMAC_close(ctx) + #elif defined(USE_GNUTLS) #include #ifndef SHA256_DIGEST_LENGTH @@ -51,6 +65,7 @@ #define HMAC_crunch(ctx, buf, len) hmac_sha256_update(&ctx, len, buf) #define HMAC_finish(ctx, dig, dlen) dlen = SHA256_DIGEST_LENGTH; hmac_sha256_digest(&ctx, SHA256_DIGEST_LENGTH, dig) #define HMAC_close(ctx) + #else /* USE_OPENSSL */ #include #include @@ -161,8 +176,17 @@ HTTP_get(struct HTTP_ctx *http, const char *url, HTTP_read_callback *cb) goto leave; #else TLS_client(RTMP_TLS_ctx, sb.sb_ssl); + +#if defined(USE_MBEDTLS) + mbedtls_net_context *server_fd = &RTMP_TLS_ctx->net; + server_fd->fd = sb.sb_socket; + TLS_setfd(sb.sb_ssl, server_fd); +#else TLS_setfd(sb.sb_ssl, sb.sb_socket); - if (TLS_connect(sb.sb_ssl) < 0) +#endif + + int connect_return = TLS_connect(sb.sb_ssl); + if (connect_return < 0) { RTMP_Log(RTMP_LOGERROR, "%s, TLS_Connect failed", __FUNCTION__); ret = HTTPRES_LOST_CONNECTION; @@ -318,21 +342,21 @@ swfcrunch(void *ptr, size_t size, size_t nmemb, void *stream) { unsigned char out[CHUNK]; i->zs->next_in = (unsigned char *)p; - i->zs->avail_in = len; + i->zs->avail_in = (uInt)len; do { i->zs->avail_out = CHUNK; i->zs->next_out = out; inflate(i->zs, Z_NO_FLUSH); len = CHUNK - i->zs->avail_out; - i->size += len; + i->size += (int)len; HMAC_crunch(i->ctx, out, len); } while (i->zs->avail_out == 0); } else { - i->size += len; + i->size += (int)len; HMAC_crunch(i->ctx, (unsigned char *)p, len); } return size * nmemb; @@ -469,7 +493,7 @@ RTMP_HashSWF(const char *url, unsigned int *size, unsigned char *hash, home.av_val = "\\UserData"; #else hpre.av_val = getenv("HOMEDRIVE"); - hpre.av_len = strlen(hpre.av_val); + hpre.av_len = (int)strlen(hpre.av_val); home.av_val = getenv("HOMEPATH"); #endif #define DIRSEP "\\" @@ -482,7 +506,7 @@ RTMP_HashSWF(const char *url, unsigned int *size, unsigned char *hash, #endif if (!home.av_val) home.av_val = "."; - home.av_len = strlen(home.av_val); + home.av_len = (int)strlen(home.av_val); /* SWF hash info is cached in a fixed-format file. * url: @@ -528,7 +552,7 @@ RTMP_HashSWF(const char *url, unsigned int *size, unsigned char *hash, if (strncmp(buf + 5, url, hlen)) continue; r1 = strrchr(buf, '/'); - i = strlen(r1); + i = (int)strlen(r1); r1[--i] = '\0'; if (strncmp(r1, file, i)) continue; @@ -543,7 +567,7 @@ RTMP_HashSWF(const char *url, unsigned int *size, unsigned char *hash, else if (!strncmp(buf, "hash: ", 6)) { unsigned char *ptr = hash, *in = (unsigned char *)buf + 6; - int l = strlen((char *)in) - 1; + int l = (int)strlen((char *)in) - 1; for (i = 0; i < l; i += 2) *ptr++ = (HEX2BIN(in[i]) << 4) | HEX2BIN(in[i + 1]); got++; @@ -625,7 +649,7 @@ RTMP_HashSWF(const char *url, unsigned int *size, unsigned char *hash, if (q) i = q - url; else - i = strlen(url); + i = (int)strlen(url); fprintf(f, "url: %.*s\n", i, url); } diff --git a/plugins/obs-outputs/librtmp/rtmp.c b/plugins/obs-outputs/librtmp/rtmp.c index dccad0e..09e965e 100644 --- a/plugins/obs-outputs/librtmp/rtmp.c +++ b/plugins/obs-outputs/librtmp/rtmp.c @@ -40,7 +40,20 @@ #pragma GCC diagnostic ignored "-Wdeprecated-declarations" #endif -#ifdef USE_POLARSSL +#if defined(USE_MBEDTLS) +#if defined(_WIN32) +#include +#include +#elif defined(__APPLE__) +#include +#endif + +#include +#include +#include +#define MD5_DIGEST_LENGTH 16 + +#elif defined(USE_POLARSSL) #include #include #include @@ -57,7 +70,6 @@ static const char *my_dhm_P = "E8A700D60B7F1200FA8E77B0A979DABF"; static const char *my_dhm_G = "4"; - #elif defined(USE_GNUTLS) #include #define MD5_DIGEST_LENGTH 16 @@ -70,7 +82,8 @@ static const char *my_dhm_G = "4"; #include #include #endif -TLS_CTX RTMP_TLS_ctx; + +TLS_CTX RTMP_TLS_ctx = NULL; #endif #define RTMP_SIG_SIZE 1536 @@ -226,9 +239,13 @@ RTMPPacket_Reset(RTMPPacket *p) } int -RTMPPacket_Alloc(RTMPPacket *p, int nSize) +RTMPPacket_Alloc(RTMPPacket *p, uint32_t nSize) { - char *ptr = calloc(1, nSize + RTMP_MAX_HEADER_SIZE); + char *ptr; + if (nSize > SIZE_MAX - RTMP_MAX_HEADER_SIZE) + return FALSE; + + ptr = calloc(1, nSize + RTMP_MAX_HEADER_SIZE); if (!ptr) return FALSE; p->m_body = ptr + RTMP_MAX_HEADER_SIZE; @@ -261,11 +278,101 @@ RTMP_LibVersion() return RTMP_LIB_VERSION; } +void +RTMP_TLS_LoadCerts() { +#ifdef USE_MBEDTLS + mbedtls_x509_crt *chain = RTMP_TLS_ctx->cacert = calloc(1, sizeof(struct mbedtls_x509_crt)); + mbedtls_x509_crt_init(chain); + +#if defined(_WIN32) + HCERTSTORE hCertStore; + PCCERT_CONTEXT pCertContext = NULL; + + if (!(hCertStore = CertOpenSystemStore((HCRYPTPROV)NULL, L"ROOT"))) { + goto error; + } + + while (pCertContext = CertEnumCertificatesInStore(hCertStore, pCertContext)) { + mbedtls_x509_crt_parse_der(chain, + (unsigned char *)pCertContext->pbCertEncoded, + pCertContext->cbCertEncoded); + } + + CertFreeCertificateContext(pCertContext); + CertCloseStore(hCertStore, 0); +#elif defined(__APPLE__) + SecKeychainRef keychain_ref; + CFMutableDictionaryRef search_settings_ref; + CFArrayRef result_ref; + + if (SecKeychainOpen("/System/Library/Keychains/SystemRootCertificates.keychain", + &keychain_ref) + != errSecSuccess) { + goto error; + } + + search_settings_ref = CFDictionaryCreateMutable(NULL, 0, NULL, NULL); + CFDictionarySetValue(search_settings_ref, kSecClass, kSecClassCertificate); + CFDictionarySetValue(search_settings_ref, kSecMatchLimit, kSecMatchLimitAll); + CFDictionarySetValue(search_settings_ref, kSecReturnRef, kCFBooleanTrue); + CFDictionarySetValue(search_settings_ref, kSecMatchSearchList, + CFArrayCreate(NULL, (const void **)&keychain_ref, 1, NULL)); + + if (SecItemCopyMatching(search_settings_ref, (CFTypeRef *)&result_ref) + != errSecSuccess) { + goto error; + } + + for (CFIndex i = 0; i < CFArrayGetCount(result_ref); i++) { + SecCertificateRef item_ref = (SecCertificateRef) + CFArrayGetValueAtIndex(result_ref, i); + CFDataRef data_ref; + + if ((data_ref = SecCertificateCopyData(item_ref))) { + mbedtls_x509_crt_parse_der(chain, + (unsigned char *)CFDataGetBytePtr(data_ref), + CFDataGetLength(data_ref)); + CFRelease(data_ref); + } + } + + CFRelease(keychain_ref); +#elif defined(__linux__) + if (mbedtls_x509_crt_parse_path(chain, "/etc/ssl/certs/") != 0) { + goto error; + } +#endif + + mbedtls_ssl_conf_ca_chain(&RTMP_TLS_ctx->conf, chain, NULL); + return; + +error: + mbedtls_x509_crt_free(chain); + free(chain); + RTMP_TLS_ctx->cacert = NULL; +#endif /* USE_MBEDTLS */ +} + void RTMP_TLS_Init() { #ifdef CRYPTO -#ifdef USE_POLARSSL +#if defined(USE_MBEDTLS) + const char * pers = "RTMP_TLS"; + RTMP_TLS_ctx = calloc(1,sizeof(struct tls_ctx)); + + mbedtls_ssl_config_init(&RTMP_TLS_ctx->conf); + mbedtls_ctr_drbg_init(&RTMP_TLS_ctx->ctr_drbg); + mbedtls_entropy_init(&RTMP_TLS_ctx->entropy); + + mbedtls_ctr_drbg_seed(&RTMP_TLS_ctx->ctr_drbg, + mbedtls_entropy_func, + &RTMP_TLS_ctx->entropy, + (const unsigned char *)pers, + strlen(pers)); + + RTMP_TLS_LoadCerts(); +#elif defined(USE_POLARSSL) /* Do this regardless of NO_SSL, we use havege for rtmpe too */ RTMP_TLS_ctx = calloc(1,sizeof(struct tls_ctx)); havege_init(&RTMP_TLS_ctx->hs); @@ -289,6 +396,26 @@ RTMP_TLS_Init() SSL_CTX_set_options(RTMP_TLS_ctx, SSL_OP_ALL); SSL_CTX_set_default_verify_paths(RTMP_TLS_ctx); #endif +#else +#endif +} + +void +RTMP_TLS_Free() { +#ifdef USE_MBEDTLS + mbedtls_ssl_config_free(&RTMP_TLS_ctx->conf); + mbedtls_ctr_drbg_free(&RTMP_TLS_ctx->ctr_drbg); + mbedtls_entropy_free(&RTMP_TLS_ctx->entropy); + + if (RTMP_TLS_ctx->cacert) { + mbedtls_x509_crt_free(RTMP_TLS_ctx->cacert); + free(RTMP_TLS_ctx->cacert); + RTMP_TLS_ctx->cacert = NULL; + } + + // NO mbedtls_net_free() BECAUSE WE SET IT UP BY HAND! + free(RTMP_TLS_ctx); + RTMP_TLS_ctx = NULL; #endif } @@ -299,7 +426,27 @@ RTMP_TLS_AllocServerContext(const char* cert, const char* key) #ifdef CRYPTO if (!RTMP_TLS_ctx) RTMP_TLS_Init(); -#ifdef USE_POLARSSL +#if defined(USE_MBEDTLS) + tls_server_ctx *tc = ctx = calloc(1, sizeof(struct tls_server_ctx)); + tc->conf = &RTMP_TLS_ctx->conf; + tc->ctr_drbg = &RTMP_TLS_ctx->ctr_drbg; + + mbedtls_x509_crt_init(&tc->cert); + if (mbedtls_x509_crt_parse_file(&tc->cert, cert)) + { + free(tc); + return NULL; + } + + mbedtls_pk_init(&tc->key); + if (mbedtls_pk_parse_keyfile(&tc->key, key, NULL)) + { + mbedtls_x509_crt_free(&tc->cert); + mbedtls_pk_free(&tc->key); + free(tc); + return NULL; + } +#elif defined(USE_POLARSSL) tls_server_ctx *tc = ctx = calloc(1, sizeof(struct tls_server_ctx)); tc->dhm_P = my_dhm_P; tc->dhm_G = my_dhm_G; @@ -346,7 +493,11 @@ void RTMP_TLS_FreeServerContext(void *ctx) { #ifdef CRYPTO -#ifdef USE_POLARSSL +#if defined(USE_MBEDTLS) + mbedtls_x509_crt_free(&((tls_server_ctx*)ctx)->cert); + mbedtls_pk_free(&((tls_server_ctx*)ctx)->key); + free(ctx); +#elif defined(USE_POLARSSL) x509_free(&((tls_server_ctx*)ctx)->cert); rsa_free(&((tls_server_ctx*)ctx)->key); free(ctx); @@ -370,6 +521,10 @@ RTMP_Alloc() void RTMP_Free(RTMP *r) { +#if defined(CRYPTO) && defined(USE_MBEDTLS) + if (RTMP_TLS_ctx) + RTMP_TLS_Free(); +#endif free(r); } @@ -846,9 +1001,20 @@ int RTMP_TLS_Accept(RTMP *r, void *ctx) { #if defined(CRYPTO) && !defined(NO_SSL) - TLS_server(ctx, r->m_sb.sb_ssl); + tls_server_ctx *srv_ctx = ctx; + TLS_server(srv_ctx, r->m_sb.sb_ssl); + +#if defined(USE_MBEDTLS) + mbedtls_net_context *client_fd = &RTMP_TLS_ctx->net; + mbedtls_net_init(client_fd); + client_fd->fd = r->m_sb.sb_socket; + TLS_setfd(r->m_sb.sb_ssl, client_fd); +#else TLS_setfd(r->m_sb.sb_ssl, r->m_sb.sb_socket); - if (TLS_accept(r->m_sb.sb_ssl) < 0) +#endif + + int connect_return = TLS_connect(r->m_sb.sb_ssl); + if (connect_return < 0) { RTMP_Log(RTMP_LOGERROR, "%s, TLS_Connect failed", __FUNCTION__); return FALSE; @@ -868,10 +1034,59 @@ RTMP_Connect1(RTMP *r, RTMPPacket *cp) { #if defined(CRYPTO) && !defined(NO_SSL) TLS_client(RTMP_TLS_ctx, r->m_sb.sb_ssl); + +#if defined(USE_MBEDTLS) + mbedtls_net_context *server_fd = &RTMP_TLS_ctx->net; + server_fd->fd = r->m_sb.sb_socket; + TLS_setfd(r->m_sb.sb_ssl, server_fd); + + // make sure we verify the certificate hostname + char hostname[MBEDTLS_SSL_MAX_HOST_NAME_LEN + 1]; + + if (r->Link.hostname.av_len >= MBEDTLS_SSL_MAX_HOST_NAME_LEN) + return FALSE; + + memcpy(hostname, r->Link.hostname.av_val, r->Link.hostname.av_len); + hostname[r->Link.hostname.av_len] = 0; + + if (mbedtls_ssl_set_hostname(r->m_sb.sb_ssl, hostname)) + return FALSE; +#else TLS_setfd(r->m_sb.sb_ssl, r->m_sb.sb_socket); - if (TLS_connect(r->m_sb.sb_ssl) < 0) +#endif + + int connect_return = TLS_connect(r->m_sb.sb_ssl); + if (connect_return < 0) { - RTMP_Log(RTMP_LOGERROR, "%s, TLS_Connect failed", __FUNCTION__); +#if defined(USE_MBEDTLS) + if (connect_return == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) + { + r->last_error_code = connect_return; + + // show a more detailed error in the log if possible + int verify_result = mbedtls_ssl_get_verify_result(r->m_sb.sb_ssl); + if (verify_result) + { + char err[256], *e; + if (mbedtls_x509_crt_verify_info(err, sizeof(err), "", verify_result) > 0) + { + e = strchr(err, '\n'); + if (e) + *e = '\0'; + } + else + { + strcpy(err, "unknown error"); + } + RTMP_Log(RTMP_LOGERROR, "%s, Cert verify failed: %d (%s)", __FUNCTION__, verify_result, err); + RTMP_Close(r); + return FALSE; + } + } +#endif + // output the error in a format that matches mbedTLS + connect_return = abs(connect_return); + RTMP_Log(RTMP_LOGERROR, "%s, TLS_Connect failed: -0x%x", __FUNCTION__, connect_return); RTMP_Close(r); return FALSE; } @@ -879,7 +1094,6 @@ RTMP_Connect1(RTMP *r, RTMPPacket *cp) RTMP_Log(RTMP_LOGERROR, "%s, no SSL/TLS support", __FUNCTION__); RTMP_Close(r); return FALSE; - #endif } if (r->Link.protocol & RTMP_FEATURE_HTTP) @@ -1108,7 +1322,7 @@ RTMP_GetNextMediaPacket(RTMP *r, RTMPPacket *packet) while (!bHasMediaPacket && RTMP_IsConnected(r) && RTMP_ReadPacket(r, packet)) { - if (!RTMPPacket_IsReady(packet)) + if (!RTMPPacket_IsReady(packet) || !packet->m_nBodySize) { continue; } @@ -2398,7 +2612,19 @@ b64enc(const unsigned char *input, int length, char *output, int maxsize) { (void)maxsize; -#ifdef USE_POLARSSL +#if defined(USE_MBEDTLS) + size_t osize; + if(mbedtls_base64_encode((unsigned char *) output, maxsize, &osize, input, length) == 0) + { + output[osize] = '\0'; + return 1; + } + else + { + RTMP_Log(RTMP_LOGDEBUG, "%s, error", __FUNCTION__); + return 0; + } +#elif defined(USE_POLARSSL) size_t buf_size = maxsize; if(base64_encode((unsigned char *) output, &buf_size, input, length) == 0) { @@ -2457,7 +2683,13 @@ b64enc(const unsigned char *input, int length, char *output, int maxsize) return 1; } -#ifdef USE_POLARSSL +#if defined(USE_MBEDTLS) +typedef mbedtls_md5_context MD5_CTX; +#define MD5_Init(ctx) mbedtls_md5_init(ctx); mbedtls_md5_starts(ctx) +#define MD5_Update(ctx,data,len) mbedtls_md5_update(ctx,(unsigned char *)data,len) +#define MD5_Final(dig,ctx) mbedtls_md5_finish(ctx,dig); mbedtls_md5_free(ctx) + +#elif defined(USE_POLARSSL) #define MD5_CTX md5_context #define MD5_Init(ctx) md5_starts(ctx) #define MD5_Update(ctx,data,len) md5_update(ctx,(unsigned char *)data,len) @@ -3713,7 +3945,6 @@ RTMP_ReadPacket(RTMP *r, RTMPPacket *packet) { packet->m_nBodySize = AMF_DecodeInt24(header + 3); packet->m_nBytesRead = 0; - RTMPPacket_Free(packet); if (nSize > 6) { diff --git a/plugins/obs-outputs/librtmp/rtmp.h b/plugins/obs-outputs/librtmp/rtmp.h index 0975190..bd3abe6 100644 --- a/plugins/obs-outputs/librtmp/rtmp.h +++ b/plugins/obs-outputs/librtmp/rtmp.h @@ -150,7 +150,7 @@ extern "C" void RTMPPacket_Reset(RTMPPacket *p); void RTMPPacket_Dump(RTMPPacket *p); - int RTMPPacket_Alloc(RTMPPacket *p, int nSize); + int RTMPPacket_Alloc(RTMPPacket *p, uint32_t nSize); void RTMPPacket_Free(RTMPPacket *p); #define RTMPPacket_IsReady(a) ((a)->m_nBytesRead == (a)->m_nBodySize) diff --git a/plugins/obs-outputs/librtmp/rtmp_sys.h b/plugins/obs-outputs/librtmp/rtmp_sys.h index 098478c..1e8e4dc 100644 --- a/plugins/obs-outputs/librtmp/rtmp_sys.h +++ b/plugins/obs-outputs/librtmp/rtmp_sys.h @@ -80,7 +80,76 @@ #include "rtmp.h" -#ifdef USE_POLARSSL +#if defined(USE_MBEDTLS) +#include +#include +#include +#include +#include + +#define my_dhm_P \ + "E4004C1F94182000103D883A448B3F80" \ + "2CE4B44A83301270002C20D0321CFD00" \ + "11CCEF784C26A400F43DFB901BCA7538" \ + "F2C6B176001CF5A0FD16D2C48B1D0C1C" \ + "F6AC8E1DA6BCC3B4E1F96B0564965300" \ + "FFA1D0B601EB2800F489AA512C4B248C" \ + "01F76949A60BB7F00A40B1EAB64BDD48" \ + "E8A700D60B7F1200FA8E77B0A979DABF" + +#define my_dhm_G "4" + +#define SSL_SET_SESSION(S,resume,timeout,ctx) mbedtls_ssl_set_session(S,ctx) + +typedef struct tls_ctx +{ + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_ssl_config conf; + mbedtls_ssl_session ssn; + mbedtls_x509_crt *cacert; + mbedtls_net_context net; +} tls_ctx; + +typedef struct tls_server_ctx +{ + mbedtls_ssl_config *conf; + mbedtls_ctr_drbg_context *ctr_drbg; + mbedtls_pk_context key; + mbedtls_x509_crt cert; +} tls_server_ctx; + +typedef tls_ctx *TLS_CTX; + +#define TLS_client(ctx,s) \ + s = malloc(sizeof(mbedtls_ssl_context));\ + mbedtls_ssl_init(s);\ + mbedtls_ssl_setup(s, &ctx->conf);\ + mbedtls_ssl_config_defaults(&ctx->conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT);\ + mbedtls_ssl_conf_authmode(&ctx->conf, MBEDTLS_SSL_VERIFY_REQUIRED);\ + mbedtls_ssl_conf_rng(&ctx->conf, mbedtls_ctr_drbg_random, &ctx->ctr_drbg) + +#define TLS_server(ctx,s)\ + s = malloc(sizeof(mbedtls_ssl_context));\ + mbedtls_ssl_init(s);\ + mbedtls_ssl_setup(s, ctx->conf);\ + mbedtls_ssl_conf_endpoint(ctx->conf, MBEDTLS_SSL_IS_SERVER);\ + mbedtls_ssl_conf_authmode(ctx->conf, MBEDTLS_SSL_VERIFY_REQUIRED);\ + mbedtls_ssl_conf_rng(ctx->conf, mbedtls_ctr_drbg_random, ctx->ctr_drbg);\ + mbedtls_ssl_conf_own_cert(ctx->conf, &ctx->cert, &ctx->key);\ + mbedtls_ssl_conf_dh_param_bin(ctx->conf,\ + (const unsigned char *)my_dhm_P, strlen(my_dhm_P),\ + (const unsigned char *)my_dhm_G, strlen(my_dhm_G)) + +#define TLS_setfd(s,fd) mbedtls_ssl_set_bio(s, fd, mbedtls_net_send, mbedtls_net_recv, NULL) +#define TLS_connect(s) mbedtls_ssl_handshake(s) +#define TLS_accept(s) mbedtls_ssl_handshake(s) +#define TLS_read(s,b,l) mbedtls_ssl_read(s,(unsigned char *)b,l) +#define TLS_write(s,b,l) mbedtls_ssl_write(s,(unsigned char *)b,l) +#define TLS_shutdown(s) mbedtls_ssl_close_notify(s) +#define TLS_close(s) mbedtls_ssl_free(s); free(s) + +#elif defined(USE_POLARSSL) #include #include #include @@ -128,6 +197,7 @@ typedef struct tls_server_ctx #define TLS_shutdown(s) ssl_close_notify(s) #define TLS_close(s) ssl_free(s); free(s) + #elif defined(USE_GNUTLS) #include typedef struct tls_ctx diff --git a/plugins/obs-outputs/rtmp-stream.c b/plugins/obs-outputs/rtmp-stream.c index d33969e..def2788 100644 --- a/plugins/obs-outputs/rtmp-stream.c +++ b/plugins/obs-outputs/rtmp-stream.c @@ -438,6 +438,15 @@ static void set_output_error(struct rtmp_stream *stream) } #endif + // non platform-specific errors + if (!msg) { + switch (stream->rtmp.last_error_code) { + case -0x2700: + msg = obs_module_text("SSLCertVerifyFailed"); + break; + } + } + obs_output_set_last_error(stream->output, msg); } diff --git a/plugins/obs-outputs/rtmp-windows.c b/plugins/obs-outputs/rtmp-windows.c index 64e8ec0..cd0deb8 100644 --- a/plugins/obs-outputs/rtmp-windows.c +++ b/plugins/obs-outputs/rtmp-windows.c @@ -169,13 +169,13 @@ static enum data_ret write_data(struct rtmp_stream *stream, bool *can_write, size_t send_len = min(latency_packet_size, stream->write_buf_len); - ret = send(stream->rtmp.m_sb.sb_socket, + ret = RTMPSockBuf_Send(&stream->rtmp.m_sb, (const char *)stream->write_buf, - (int)send_len, 0); + (int)send_len); } else { - ret = send(stream->rtmp.m_sb.sb_socket, + ret = RTMPSockBuf_Send(&stream->rtmp.m_sb, (const char *)stream->write_buf, - (int)stream->write_buf_len, 0); + (int)stream->write_buf_len); } if (ret > 0) { diff --git a/plugins/obs-text/data/locale/el-GR.ini b/plugins/obs-text/data/locale/el-GR.ini index 0dd9856..0b070f7 100644 --- a/plugins/obs-text/data/locale/el-GR.ini +++ b/plugins/obs-text/data/locale/el-GR.ini @@ -7,6 +7,12 @@ Filter.TextFiles="Αρχεία κειμένου" Filter.AllFiles="Όλα τα αρχεία" Color="Χρώμα" Opacity="Αδιαφάνεια" +Gradient="Κλίση" +Gradient.Color="Χρώμα κλίσης" +Gradient.Opacity="Αδιαφάνεια κλίσης" +Gradient.Direction="Κατεύθυνση διαβάθμισης" +BkColor="Χρώμα Φόντου" +BkOpacity="Αδιαφάνεια Φόντου" Alignment="Στοίχιση" Alignment.Left="Αριστερά" Alignment.Center="Κέντρο" @@ -16,4 +22,13 @@ VerticalAlignment="Κατακόρυφη Στοίχιση" VerticalAlignment.Top="Πάνω" VerticalAlignment.Bottom="Κάτω" Outline="Περίγραμμα" +Outline.Size="Μέγεθος περιγράμματος" +Outline.Color="Χρώμα Περιγράμματος" +Outline.Opacity="Αδιαφάνεια περιγράμματος" +ChatlogMode="Λειτουργία αρχείοz καταγραφής συνομιλίας" +ChatlogMode.Lines="Όριο καταγραφής συνομιλίας" +UseCustomExtents="Χρήση Προσαρμοσμένων Επεκτάσεων Κειμένου" +UseCustomExtents.Wrap="Αναδίπλωση" +Width="Πλάτος" +Height="Ύψος" diff --git a/plugins/obs-text/data/locale/gd-GB.ini b/plugins/obs-text/data/locale/gd-GB.ini new file mode 100644 index 0000000..b2a7571 --- /dev/null +++ b/plugins/obs-text/data/locale/gd-GB.ini @@ -0,0 +1,20 @@ +Font="Cruth-clò" +Text="Teacsa" +Filter.TextFiles="Faidhlichean teacsa" +Filter.AllFiles="A h-uile faidhle" +Color="Dath" +Gradient="Caisead" +BkColor="Dath a’ chùlaibh" +Alignment="Co-thaobhadh" +Alignment.Left="Gu clì" +Alignment.Center="Sa mheadhan" +Alignment.Right="Gu deas" +Vertical="Inghearach" +VerticalAlignment="Co-thaobhadh inghearach" +VerticalAlignment.Top="Barr" +VerticalAlignment.Bottom="Bonn" +Outline="Oir-loidhne" +UseCustomExtents.Wrap="Paisg" +Width="Leud" +Height="Àirde" + diff --git a/plugins/obs-text/data/locale/ka-GE.ini b/plugins/obs-text/data/locale/ka-GE.ini new file mode 100644 index 0000000..37643c8 --- /dev/null +++ b/plugins/obs-text/data/locale/ka-GE.ini @@ -0,0 +1,34 @@ +TextGDIPlus="Text (GDI+)" +Font="შრიფტი" +Text="ტექსტი" +ReadFromFile="ფაილიდან წაკითხვა" +TextFile="ტექსტური ფაილი (UTF-8)" +Filter.TextFiles="ტექსტური ფაილები" +Filter.AllFiles="ყველა ფაილი" +Color="ფერი" +Opacity="გაუმჭვირვალობა" +Gradient="გრადიენტი" +Gradient.Color="გრადიენტის ფერი" +Gradient.Opacity="გრადიენტის გაუმჭვირვალობა" +Gradient.Direction="გრადიენტის მიმართულება" +BkColor="ფონის ფერი" +BkOpacity="ფონის გაუმჭვივალობა" +Alignment="განლაგება" +Alignment.Left="მარცხნივ" +Alignment.Center="შუაში" +Alignment.Right="მარჯვნივ" +Vertical="შვეულად" +VerticalAlignment="შვეული განლაგება" +VerticalAlignment.Top="ზემოთ" +VerticalAlignment.Bottom="ქვემოთ" +Outline="გარემოხაზულობა" +Outline.Size="გარემოხაზულობის ზომა" +Outline.Color="გარემოხაზულობის ფერი" +Outline.Opacity="გარემოხაზულობის გაუმჭვირვალობა" +ChatlogMode="სასაუბროს (Chatlog) რეჟიმი" +ChatlogMode.Lines="სასაუბროს ხაზების შეზღუდვა" +UseCustomExtents="ტექსტის ველის მითითებული ზომის გამოყენება" +UseCustomExtents.Wrap="ხაზზე გადატანა" +Width="სიგანე" +Height="სიმაღლე" + diff --git a/plugins/obs-transitions/data/locale/bg-BG.ini b/plugins/obs-transitions/data/locale/bg-BG.ini new file mode 100644 index 0000000..51fb0ed --- /dev/null +++ b/plugins/obs-transitions/data/locale/bg-BG.ini @@ -0,0 +1,21 @@ +CutTransition="Изрязване" +Direction="Посока" +Color="Цвят" +VideoFile="Видеофайл" +TransitionPointTypeFrame="Рамка" +TransitionPointTypeTime="Време (в милисекунди)" +LumaWipe.Image="Изображение" +LumaWipe.Type.Burst="Избухване" +LumaWipe.Type.CheckerboardSmall="Шахматна дъсчица" +LumaWipe.Type.Circles="Кръгове" +LumaWipe.Type.Clock="Часовник" +LumaWipe.Type.Cloud="Облак" +LumaWipe.Type.Fan="Вентилатор" +LumaWipe.Type.Fractal="Фрактал" +LumaWipe.Type.Iris="Ирис" +LumaWipe.Type.Square="Квадрат" +LumaWipe.Type.Squares="Квадрати" +LumaWipe.Type.Stripes="Ивици" +LumaWipe.Type.ZigzagHorizontal="Водоравен зигзаг" +LumaWipe.Type.ZigzagVertical="Отвесен зигзаг" + diff --git a/plugins/obs-transitions/data/locale/el-GR.ini b/plugins/obs-transitions/data/locale/el-GR.ini index 34d22ba..d1961ce 100644 --- a/plugins/obs-transitions/data/locale/el-GR.ini +++ b/plugins/obs-transitions/data/locale/el-GR.ini @@ -1,9 +1,66 @@ FadeTransition="Ξεθώριασμα" CutTransition="Αποκοπή" +SwipeTransition="Σύρσιμο" +SlideTransition="Ολίσθηση" +StingerTransition="Τσούξιμο" +FadeToColorTransition="Ξεθωριάζει το Χρώμα" +Direction="Κατεύθυνση" Direction.Left="Αριστερά" Direction.Right="Δεξιά" +Direction.Up="Επάνω" +Direction.Down="Κάτω" +SwipeIn="Σύρετε προς τα επάνω" Color="Χρώμα" VideoFile="Αρχείο Βίντεο" +TransitionPoint="Ταχύτητα μετάβασης (χιλιοστά δευτερολέπτου)" +TransitionPointFrame="Σημείο μετάβασης (πλαίσιο)" +TransitionPointType="Τύπος σημείου μετάβασης" TransitionPointTypeFrame="Καρέ" TransitionPointTypeTime="Χρόνος (χιλιοστά δευτερολέπτου)" +AudioFadeStyle="Στυλ ήχου Fade" +AudioFadeStyle.FadeOutFadeIn="Fade out στο σημείο μετάβασης στη συνέχεια, fade in" +AudioFadeStyle.CrossFade="Σταδιακή Εξασθένιση" +SwitchPoint="Peak Χρώμα Σημείου (ποσοστό)" +LumaWipeTransition="Luma Wipe" +LumaWipe.Image="Εικόνα" +LumaWipe.Invert="Αντιστροφή" +LumaWipe.Softness="Απαλότητα" +LumaWipe.Type.BarndoorBottomLeft="Barndoor Κάτω Αριστερά" +LumaWipe.Type.BarndoorHorizontal="Barndoor οριζόντια" +LumaWipe.Type.BarndoorTopLeft="Barndoor Πάνω Αριστερά" +LumaWipe.Type.BarndoorVertical="Barndoor Κάθετα" +LumaWipe.Type.BlindsHorizontal="Barndoor οριζόντια" +LumaWipe.Type.BoxBottomLeft="Κουτί Κάτω Αριστερά" +LumaWipe.Type.BoxBottomRight="Κουτί Κάτω Δεξιά" +LumaWipe.Type.BoxTopLeft="Κουτί Πάνω Αριστερά" +LumaWipe.Type.BoxTopRight="Κουτί Πάνω Δεξιά" +LumaWipe.Type.Burst="Έκρηξη" +LumaWipe.Type.CheckerboardSmall="Μικρή σκακιέρα" +LumaWipe.Type.Circles="Κύκλοι" +LumaWipe.Type.Clock="Ρολόι" +LumaWipe.Type.Cloud="Σύννεφο" +LumaWipe.Type.Curtain="Κουρτίνες" +LumaWipe.Type.Fan="Ανεμιστήρας" +LumaWipe.Type.Fractal="Φράκταλ" +LumaWipe.Type.Iris="Ίρις" +LumaWipe.Type.LinearHorizontal="Γραμμική Οριζόντια" +LumaWipe.Type.LinearTopLeft="Γραμμική Πάνω Αριστερά" +LumaWipe.Type.LinearTopRight="Γραμμική Πάνω Δεξιά" +LumaWipe.Type.LinearVertical="Γραμμική Κατακόρυφα" +LumaWipe.Type.ParallelZigzagHorizontal="Παράλληλα Ζιγκ-Ζαγκ Οριζόντια" +LumaWipe.Type.ParallelZigzagVertical="Παράλληλα Ζιγκ-Ζαγκ Κάθετα" +LumaWipe.Type.Sinus9="Sinus 9" +LumaWipe.Type.Spiral="Σπείρα" +LumaWipe.Type.Square="Τετράγωνο" +LumaWipe.Type.Squares="Τετράγωνα" +LumaWipe.Type.Stripes="Ρίγες" +LumaWipe.Type.StripsHorizontal="Οριζόντια Αναστροφή" +LumaWipe.Type.StripsVertical="Κάθετη Αναστροφή" +LumaWipe.Type.Watercolor="Ακουαρέλα" +LumaWipe.Type.ZigzagHorizontal="Γραμμική Οριζόντια" +LumaWipe.Type.ZigzagVertical="Γραμμική Κατακόρυφα" +AudioMonitoring="Ηχητική παρακολούθηση" +AudioMonitoring.None="Monitor Off" +AudioMonitoring.MonitorOnly="Μόνο η οθόνη (σίγαση εξόδου)" +AudioMonitoring.Both="Παρακολούθηση και έξοδος" diff --git a/plugins/obs-transitions/data/locale/gd-GB.ini b/plugins/obs-transitions/data/locale/gd-GB.ini new file mode 100644 index 0000000..531b1c3 --- /dev/null +++ b/plugins/obs-transitions/data/locale/gd-GB.ini @@ -0,0 +1,26 @@ +FadeTransition="Crìon" +CutTransition="Gearr às" +SwipeTransition="Grad-shlaighd" +SlideTransition="Sleamhnaich" +Direction="Comhair" +Direction.Left="Gu clì" +Direction.Right="Gu deas" +Direction.Up="Suas" +Direction.Down="Sìos" +SwipeIn="Grad-shlaighd a-steach" +Color="Dath" +AudioFadeStyle.CrossFade="Tar-chrìonadh" +LumaWipe.Image="Dealbh" +LumaWipe.Invert="Ais-thionndaidh" +LumaWipe.Type.Circles="Cearcallan" +LumaWipe.Type.Clock="Cleoc" +LumaWipe.Type.Cloud="Neul" +LumaWipe.Type.Square="Ceàrnag" +LumaWipe.Type.Squares="Ceàrnagan" +LumaWipe.Type.Stripes="Stiallan" +LumaWipe.Type.Watercolor="Dath-uisge" +AudioMonitoring="Sgrùdadh fuaime" +AudioMonitoring.None="Gun sgrùdadh" +AudioMonitoring.MonitorOnly="Sgrùdadh a-mhàin (mùch an t-às-chur)" +AudioMonitoring.Both="Sgrùdadh is às-chur" + diff --git a/plugins/obs-transitions/data/locale/ka-GE.ini b/plugins/obs-transitions/data/locale/ka-GE.ini new file mode 100644 index 0000000..948601b --- /dev/null +++ b/plugins/obs-transitions/data/locale/ka-GE.ini @@ -0,0 +1,66 @@ +FadeTransition="მილევა" +CutTransition="მოჭრა" +SwipeTransition="შენაცვლება" +SlideTransition="გადაწევა" +StingerTransition="Stinger" +FadeToColorTransition="ფერში გადასვლა" +Direction="მიმართულება" +Direction.Left="მარცხნივ" +Direction.Right="მარჯვნივ" +Direction.Up="ზემოთ" +Direction.Down="ქვემოთ" +SwipeIn="უძრავად შენაცვლება" +Color="ფერი" +VideoFile="ვიდეოფაილი" +TransitionPoint="გადასვლის წერტილი (მილიწამი)" +TransitionPointFrame="გადასვლის წერტილი (კადრი)" +TransitionPointType="გადასვლის წერტილის სახეობა" +TransitionPointTypeFrame="კადრი" +TransitionPointTypeTime="დრო (წამები)" +AudioFadeStyle="ხმის მილევის ნაირსახეობა" +AudioFadeStyle.FadeOutFadeIn="თანდათან მილევა გადასვლის წერტილში და შემდეგ მომატება" +AudioFadeStyle.CrossFade="ჯვარედინი გადასვლა" +SwitchPoint="ფერის უმაღლესი წერტილი (პროცენტი)" +LumaWipeTransition="Luma Wipe" +LumaWipe.Image="სურათი" +LumaWipe.Invert="შებრუნება" +LumaWipe.Softness="სირბილე" +LumaWipe.Type.BarndoorBottomLeft="მარცხნივ ქვემოთ დახრილი გადაფარვა" +LumaWipe.Type.BarndoorHorizontal="თარაზული გადაფარვა" +LumaWipe.Type.BarndoorTopLeft="მარცხნივ ზემოთ დახრილი გადაფარვა" +LumaWipe.Type.BarndoorVertical="შვეული გადაფარვა" +LumaWipe.Type.BlindsHorizontal="თარაზული ჟალუზები" +LumaWipe.Type.BoxBottomLeft="ქვედა მარცხენა კუთხიდან გადაფარვა" +LumaWipe.Type.BoxBottomRight="ქვედა მარჯვენა კუთხიდან გადაფარვა" +LumaWipe.Type.BoxTopLeft="ზედა მარცხენა კუთხიდან გადაფარვა" +LumaWipe.Type.BoxTopRight="ზედა მარჯვენა კუთხიდან გადაფარვა" +LumaWipe.Type.Burst="აფეთქება" +LumaWipe.Type.CheckerboardSmall="უჯრები" +LumaWipe.Type.Circles="წრეები" +LumaWipe.Type.Clock="საათი" +LumaWipe.Type.Cloud="ღრუბელი" +LumaWipe.Type.Curtain="ფარდა" +LumaWipe.Type.Fan="დატრიალება" +LumaWipe.Type.Fractal="ნამსხვრევები" +LumaWipe.Type.Iris="წერტილოვანი" +LumaWipe.Type.LinearHorizontal="ხაზოვანი თარაზულად" +LumaWipe.Type.LinearTopLeft="ხაზოვანი ზედა მარცხენა კუთხიდან" +LumaWipe.Type.LinearTopRight="ხაზოვანი ზედა მარჯვენა კუთხიდან" +LumaWipe.Type.LinearVertical="ხაზოვანი შვეულად" +LumaWipe.Type.ParallelZigzagHorizontal="პარალელური თარაზული ტეხილები" +LumaWipe.Type.ParallelZigzagVertical="პარალელური შვეული ტეხილები" +LumaWipe.Type.Sinus9="თხევადი" +LumaWipe.Type.Spiral="ხვეული" +LumaWipe.Type.Square="მართკუთხა" +LumaWipe.Type.Squares="მართკუთხედები" +LumaWipe.Type.Stripes="ზოლები" +LumaWipe.Type.StripsHorizontal="თარაზული ზოლები" +LumaWipe.Type.StripsVertical="შვეული ზოლები" +LumaWipe.Type.Watercolor="წყლის საღებავი" +LumaWipe.Type.ZigzagHorizontal="თარაზული ტეხილი" +LumaWipe.Type.ZigzagVertical="შვეული ტეხილი" +AudioMonitoring="ხმის მოსმენა" +AudioMonitoring.None="მოსმენის გარეშე" +AudioMonitoring.MonitorOnly="მხოლოდ მოსმენა (უხმო გამომავალი სიგნალით)" +AudioMonitoring.Both="მოსმენა და გამოტანა" + diff --git a/plugins/obs-transitions/transition-stinger.c b/plugins/obs-transitions/transition-stinger.c index 8234b0d..dc5b5a7 100644 --- a/plugins/obs-transitions/transition-stinger.c +++ b/plugins/obs-transitions/transition-stinger.c @@ -237,8 +237,8 @@ static void stinger_transition_start(void *data) (long double)s->transition_point_ns / (long double)s->duration_ns); - if (s->transition_point > 1.0f) - s->transition_point = 1.0f; + if (s->transition_point > 0.999f) + s->transition_point = 0.999f; else if (s->transition_point < 0.001f) s->transition_point = 0.001f; diff --git a/plugins/obs-x264/data/locale/gd-GB.ini b/plugins/obs-x264/data/locale/gd-GB.ini new file mode 100644 index 0000000..825e9b2 --- /dev/null +++ b/plugins/obs-x264/data/locale/gd-GB.ini @@ -0,0 +1,13 @@ +Bitrate="Reat bhiotaichean" +CustomBufsize="Cleachd meud bufair gnàthaichte" +BufferSize="Meud a’ bhufair" +RateControl="Smachd air an reat" +CRF="CRF" +KeyframeIntervalSec="Eadaramh nam frèamaichean-iuchrach (diog, fèin-obrachail)" +CPUPreset="Ro-shuidheachadh cleachdadh a’ CPU (nas àirde = nas lugha dhen CPU)" +Profile="Pròifil" +Tune="Gleus" +None="(Chan eil gin)" +EncoderOptions="Roghainnean x264 (’gan sgaradh le geal-spàs)" +VFR="Reat fhrèamaichean caochlaideach (VFR)" + diff --git a/plugins/obs-x264/data/locale/ka-GE.ini b/plugins/obs-x264/data/locale/ka-GE.ini index ea17e16..0f03e33 100644 --- a/plugins/obs-x264/data/locale/ka-GE.ini +++ b/plugins/obs-x264/data/locale/ka-GE.ini @@ -1,2 +1,13 @@ +Bitrate="ბიტური სიხშირე" +CustomBufsize="ბუფერის მითითებული ზომის გამოყენება" +BufferSize="ბუფერის ზომა" +RateControl="სიხშირის მართვა" CRF="CRF" +KeyframeIntervalSec="საკვანძო კადრებს შორის შუალედი (წამი, 0=თვითშერჩევა)" +CPUPreset="პროცესორის მოხმარების მზა პარამეტრები (მაღალი = ნაკლები CPU-დატვირთვა)" +Profile="პროფილი" +Tune="გამართვა" +None="(არცერთი)" +EncoderOptions="x264 პარამეტრები (ადგილის გამოტოვებით)" +VFR="კადრის ცვლადი სიხშირე (VFR)" diff --git a/plugins/obs-x264/data/locale/zh-CN.ini b/plugins/obs-x264/data/locale/zh-CN.ini index 5bf402c..058c829 100644 --- a/plugins/obs-x264/data/locale/zh-CN.ini +++ b/plugins/obs-x264/data/locale/zh-CN.ini @@ -6,7 +6,7 @@ CRF="CRF" KeyframeIntervalSec="关键帧间隔(秒, 0=自动)" CPUPreset="CPU 使用预设 (高 = 较少的 CPU占用)" Profile="Profile" -Tune="Tune" +Tune="协调(类型)" None="(无)" EncoderOptions="x264 选项 (用空格分隔)" VFR="可变帧率 (VFR)" diff --git a/plugins/rtmp-services/data/locale/el-GR.ini b/plugins/rtmp-services/data/locale/el-GR.ini index abebe04..54d1f51 100644 --- a/plugins/rtmp-services/data/locale/el-GR.ini +++ b/plugins/rtmp-services/data/locale/el-GR.ini @@ -2,6 +2,7 @@ StreamingServices="Υπηρεσίες Μετάδοσης" CustomStreamingServer="Προσαρμοσμένος Διακομιστής Μετάδοσης" Service="Υπηρεσία" Server="Διακομιστής" +Server.Auto="Αυτόματη (συνιστάται)" StreamKey="Κλειδί μετάδοσης" UseAuth="Χρήση πιστοποίησης" Username="Όνομα χρήστη" diff --git a/plugins/rtmp-services/data/locale/gd-GB.ini b/plugins/rtmp-services/data/locale/gd-GB.ini new file mode 100644 index 0000000..497c8fe --- /dev/null +++ b/plugins/rtmp-services/data/locale/gd-GB.ini @@ -0,0 +1,11 @@ +StreamingServices="Seirbheisean sruthaidh" +CustomStreamingServer="Frithealaiche sruthaidh gnàthaichte" +Service="Seirbheis" +Server="Frithealaiche" +Server.Auto="Fèin-obrachail (mholamaid seo)" +StreamKey="Iuchair an t-sruthaidh" +UseAuth="Cleachd dearbh-aithneachadh" +Username="Ainm-cleachdaiche" +Password="Facal-faire" +ShowAll="Seall a h-uile seirbheis" + diff --git a/plugins/rtmp-services/data/locale/ka-GE.ini b/plugins/rtmp-services/data/locale/ka-GE.ini new file mode 100644 index 0000000..a095db7 --- /dev/null +++ b/plugins/rtmp-services/data/locale/ka-GE.ini @@ -0,0 +1,11 @@ +StreamingServices="ნაკადის გაშვების მომსახურებები" +CustomStreamingServer="ნაკადის გასაშვები საკუთარი სერვერი" +Service="მომსახურება" +Server="სერვერი" +Server.Auto="ავტომატური (სასურველია)" +StreamKey="ნაკადის გასაღები" +UseAuth="ანგარიშზე შესვლის გამოყენება" +Username="მომხმარებლის სახელი" +Password="პაროლი" +ShowAll="ყველა მომსახურების ჩვენება" + diff --git a/plugins/rtmp-services/data/package.json b/plugins/rtmp-services/data/package.json index 16bd82f..0f439c3 100644 --- a/plugins/rtmp-services/data/package.json +++ b/plugins/rtmp-services/data/package.json @@ -1,10 +1,10 @@ { "url": "https://obsproject.com/obs2_update/rtmp-services", - "version": 80, + "version": 88, "files": [ { "name": "services.json", - "version": 80 + "version": 88 } ] } diff --git a/plugins/rtmp-services/data/services.json b/plugins/rtmp-services/data/services.json index 6f2e569..416ecd8 100644 --- a/plugins/rtmp-services/data/services.json +++ b/plugins/rtmp-services/data/services.json @@ -537,9 +537,27 @@ "servers": [ { "name": "Primary", - "url": "rtmp://www.gameplank.tv/live" + "url": "rtmp://live.gameplank.tv/app" + }, + { + "name": "US: Oregon", + "url": "rtmp://live-or.gameplank.tv/app" + }, + { + "name": "US: Virginia", + "url": "rtmp://live-va.gameplank.tv/app" + }, + { + "name": "UK: London", + "url": "rtmp://live-ldn.gameplank.tv/app" } - ] + ], + "recommended": { + "keyint": 1, + "max video bitrate": 1500, + "max audio bitrate": 160, + "x264opts": "scenecut=0" + } }, { "name": "Vaughn Live / iNSTAGIB", @@ -635,7 +653,7 @@ "servers": [ { "name": "Default", - "url": "rtmp://rtmp-api.facebook.com:80/rtmp/" + "url": "rtmps://rtmp-api.facebook.com:443/rtmp/" } ], "recommended": { @@ -649,6 +667,10 @@ "name": "Restream.io", "common": true, "servers": [ + { + "name": "Autodetect", + "url": "rtmp://live.restream.io/live" + }, { "name": "EU-West (London, GB)", "url": "rtmp://eu-london.restream.io/live" @@ -661,6 +683,10 @@ "name": "EU-West (Luxembourg)", "url": "rtmp://eu-luxembourg.restream.io/live" }, + { + "name": "EU-West (Paris, FR)", + "url": "rtmp://eu-paris.restream.io/live" + }, { "name": "EU-Central (Frankfurt, DE)", "url": "rtmp://eu-central.restream.io/live" @@ -797,7 +823,48 @@ "recommended": { "keyint": 2, "max video bitrate": 25000, - "max audio bitrate": 192 + "max audio bitrate": 192, + "x264opts": "scenecut=0" + } + }, + { + "name": "Castr.io", + "servers": [ + { + "name": "Chicago US", + "url": "rtmp://cg.castr.io" + }, + { + "name": "Los Angeles US", + "url": "rtmp://la.castr.io" + }, + { + "name": "Montreal CA", + "url": "rtmp://qc.castr.io" + }, + { + "name": "London UK", + "url": "rtmp://uk.castr.io" + }, + { + "name": "Frankfurt DE", + "url": "rtmp://de.castr.io" + }, + { + "name": "Moscow RU", + "url": "rtmp://ru.castr.io" + }, + { + "name": "Singapore", + "url": "rtmp://sg.castr.io" + }, + { + "name": "Sydney AU", + "url": "rtmp://au.castr.io" + } + ], + "recommended": { + "keyint": 2 } }, { @@ -1073,9 +1140,9 @@ } ], "recommended": { - "keyint": 2, - "max video bitrate": 800, - "max audio bitrate": 96 + "keyint": 3, + "max video bitrate": 4000, + "max audio bitrate": 128 } }, { @@ -1232,6 +1299,40 @@ "url": "rtmp://rtmp.cdn.asset.aparat.com:443/event" } ] + }, + { + "name": "KakaoTV", + "servers": [ + { + "name": "Default", + "url": "rtmp://rtmp.play.kakao.com/kakaotv" + } + ], + "recommended": { + "max video bitrate": 8000, + "max audio bitrate": 192 + } + }, + { + "name": "Piczel.tv", + "servers": [ + { + "name": "Default", + "url": "rtmp://piczel.tv:1935/live" + } + ], + "recommended": { + "max video bitrate": 5000 + } + }, + { + "name": "DTube", + "servers": [ + { + "name": "Default", + "url": "rtmp://stream.dtube.top/live/" + } + ] } ] } diff --git a/plugins/rtmp-services/rtmp-common.c b/plugins/rtmp-services/rtmp-common.c index 9079e07..b7bce8c 100644 --- a/plugins/rtmp-services/rtmp-common.c +++ b/plugins/rtmp-services/rtmp-common.c @@ -26,6 +26,39 @@ static inline const char *get_string_val(json_t *service, const char *key); extern void twitch_ingests_refresh(int seconds); +static void ensure_valid_url(struct rtmp_common *service, json_t *json, + obs_data_t *settings) +{ + json_t *servers = json_object_get(json, "servers"); + const char *top_url = NULL; + json_t *server; + size_t index; + + if (!service->server || !servers || !json_is_array(servers)) + return; + if (astrstri(service->service, "Facebook") == NULL) + return; + + json_array_foreach (servers, index, server) { + const char *url = get_string_val(server, "url"); + if (!url) + continue; + + if (!top_url) + top_url = url; + + if (astrcmpi(service->server, url) == 0) + return; + } + + /* server was not found in server list, use first server instead */ + if (top_url) { + bfree(service->server); + service->server = bstrdup(top_url); + obs_data_set_string(settings, "server", top_url); + } +} + static void rtmp_common_update(void *data, obs_data_t *settings) { struct rtmp_common *service = data; @@ -50,6 +83,8 @@ static void rtmp_common_update(void *data, obs_data_t *settings) if (out) service->output = bstrdup(out); } + + ensure_valid_url(service, serv, settings); } } json_decref(root); @@ -295,7 +330,7 @@ static void fill_servers(obs_property_t *servers_prop, json_t *service, obs_property_list_add_string(servers_prop, obs_module_text("Server.Auto"), "auto"); } - if (name && strcmp(name, "Twitch") == 0) { + if (strcmp(name, "Twitch") == 0) { if (fill_twitch_servers(servers_prop)) return; } diff --git a/plugins/text-freetype2/data/locale/ar-SA.ini b/plugins/text-freetype2/data/locale/ar-SA.ini index ce049f8..3339479 100644 --- a/plugins/text-freetype2/data/locale/ar-SA.ini +++ b/plugins/text-freetype2/data/locale/ar-SA.ini @@ -3,7 +3,6 @@ Font="الخط" Text="النص" TextFile="ملف نص (UTF-8 أو UTF-16)" TextFileFilter="ملفات نصية (*.txt);;" -ChatLogMode="وضع سجل المحادثة (آخر 6 سطور)" Color1="اللون الأول" Color2="اللون الثاني" Outline="حدود" diff --git a/plugins/text-freetype2/data/locale/bn-BD.ini b/plugins/text-freetype2/data/locale/bn-BD.ini index 53e2ec6..9fbd938 100644 --- a/plugins/text-freetype2/data/locale/bn-BD.ini +++ b/plugins/text-freetype2/data/locale/bn-BD.ini @@ -1,7 +1,6 @@ TextFreetype2="টেক্সট (FreeType 2)" Font="ফন্ট" TextFileFilter="টেক্সট ফাইল (*.txt);;" -ChatLogMode="আড্ডার লগ মোড (6 শেষ লাইন)" Outline="রূপরেখা" DropShadow="ছায়া ফেলে" ReadFromFile="ফাইল থেকে পড়া" diff --git a/plugins/text-freetype2/data/locale/ca-ES.ini b/plugins/text-freetype2/data/locale/ca-ES.ini index 9b8f3ec..23a6bbb 100644 --- a/plugins/text-freetype2/data/locale/ca-ES.ini +++ b/plugins/text-freetype2/data/locale/ca-ES.ini @@ -3,7 +3,8 @@ Font="Tipus de lletra" Text="Text" TextFile="Fitxer de text (UTF-8 o UTF-16)" TextFileFilter="Fitxers de text (*.txt);;" -ChatLogMode="Mode de registre de xat (últimes 6 línies)" +ChatLogMode="Mode registre de xat" +ChatLogLines="Línies del registre de xat" Color1="Color 1" Color2="Color 2" Outline="Contorn" diff --git a/plugins/text-freetype2/data/locale/cs-CZ.ini b/plugins/text-freetype2/data/locale/cs-CZ.ini index 7c6f149..004086c 100644 --- a/plugins/text-freetype2/data/locale/cs-CZ.ini +++ b/plugins/text-freetype2/data/locale/cs-CZ.ini @@ -3,7 +3,8 @@ Font="Písmo" Text="Text" TextFile="Textový soubor (UTF-8 nebo UTF-16)" TextFileFilter="Textové soubory (*.txt);;" -ChatLogMode="Mód záznamu jako chat (posledních 6 řádků)" +ChatLogMode="Režim chatu" +ChatLogLines="Počet řádků v režimu chatu" Color1="Barva 1" Color2="Barva 2" Outline="Obrys" diff --git a/plugins/text-freetype2/data/locale/da-DK.ini b/plugins/text-freetype2/data/locale/da-DK.ini index f3159f8..bff6d6e 100644 --- a/plugins/text-freetype2/data/locale/da-DK.ini +++ b/plugins/text-freetype2/data/locale/da-DK.ini @@ -3,7 +3,8 @@ Font="Skrifttype" Text="Tekst" TextFile="Tekstfil (UTF-8- eller UTF-16)" TextFileFilter="Tekstfiler (*.txt);;" -ChatLogMode="Chat log tilstand (sidste 6 linjer)" +ChatLogMode="Chatlogtilstand" +ChatLogLines="Chatloglinjer" Color1="Farve 1" Color2="Farve 2" Outline="Kontur" diff --git a/plugins/text-freetype2/data/locale/de-DE.ini b/plugins/text-freetype2/data/locale/de-DE.ini index 926cb25..c043f37 100644 --- a/plugins/text-freetype2/data/locale/de-DE.ini +++ b/plugins/text-freetype2/data/locale/de-DE.ini @@ -3,7 +3,8 @@ Font="Schriftart" Text="Text" TextFile="Textdatei (UTF-8 oder UTF-16)" TextFileFilter="Textdateien (*.txt);;" -ChatLogMode="Chat-Log-Modus (letzte 6 Zeilen)" +ChatLogMode="Chatlogmodus" +ChatLogLines="Chatlogzeilen" Color1="Farbe 1" Color2="Farbe 2" Outline="Umrandung" diff --git a/plugins/text-freetype2/data/locale/el-GR.ini b/plugins/text-freetype2/data/locale/el-GR.ini index 6b9ade9..3a4e7b8 100644 --- a/plugins/text-freetype2/data/locale/el-GR.ini +++ b/plugins/text-freetype2/data/locale/el-GR.ini @@ -3,7 +3,6 @@ Font="Γραμματοσειρά" Text="Κείμενο" TextFile="Αρχείο κειμένου (UTF-8 ή UTF-16)" TextFileFilter="Αρχεία Κειμένου (*.txt);;" -ChatLogMode="Λειτουργία καταγραφής συνομιλίας (τελευταίες 6 γραμμές)" Color1="Χρώμα 1" Color2="Χρώμα 2" Outline="Περίγραμμα" diff --git a/plugins/text-freetype2/data/locale/en-US.ini b/plugins/text-freetype2/data/locale/en-US.ini index 4a41f6f..b352775 100644 --- a/plugins/text-freetype2/data/locale/en-US.ini +++ b/plugins/text-freetype2/data/locale/en-US.ini @@ -3,7 +3,8 @@ Font="Font" Text="Text" TextFile="Text File (UTF-8 or UTF-16)" TextFileFilter="Text Files (*.txt);;" -ChatLogMode="Chat log mode (last 6 lines)" +ChatLogMode="Chat log mode" +ChatLogLines="Chat log lines" Color1="Color 1" Color2="Color 2" Outline="Outline" diff --git a/plugins/text-freetype2/data/locale/es-ES.ini b/plugins/text-freetype2/data/locale/es-ES.ini index 9220e6a..6e8ce7c 100644 --- a/plugins/text-freetype2/data/locale/es-ES.ini +++ b/plugins/text-freetype2/data/locale/es-ES.ini @@ -3,7 +3,8 @@ Font="Fuente" Text="Texto" TextFile="Archivo de texto (UTF-8 o UTF-16)" TextFileFilter="Archivos de texto (*.txt);;" -ChatLogMode="Modo de registro de chat (últimas 6 líneas)" +ChatLogMode="Modo de registro de chat" +ChatLogLines="Lineas de registro de chat" Color1="Color 1" Color2="Color 2" Outline="Contorno" diff --git a/plugins/text-freetype2/data/locale/et-EE.ini b/plugins/text-freetype2/data/locale/et-EE.ini index 79b55fa..529a723 100644 --- a/plugins/text-freetype2/data/locale/et-EE.ini +++ b/plugins/text-freetype2/data/locale/et-EE.ini @@ -3,7 +3,6 @@ Font="Font" Text="Tekst" TextFile="Tekstifail (UTF-8 või UTF-16)" TextFileFilter="Tekstifailid (*.txt);;" -ChatLogMode="Vestlus logi režiim (Viimased 6 rida)" Color1="Värv 1" Color2="Värv 2" Outline="Kontuur" diff --git a/plugins/text-freetype2/data/locale/eu-ES.ini b/plugins/text-freetype2/data/locale/eu-ES.ini index 5e322a9..5281b02 100644 --- a/plugins/text-freetype2/data/locale/eu-ES.ini +++ b/plugins/text-freetype2/data/locale/eu-ES.ini @@ -3,7 +3,8 @@ Font="Letra-tipoa" Text="Testua" TextFile="Testu-fitxategia (UTF-8 edo UTF-16)" TextFileFilter="Testu-fitxategiak (*.txt);;" -ChatLogMode="Berriketa egunkaria modua (azken 6 lerroak)" +ChatLogMode="Berriketa-egunkaria modua" +ChatLogLines="Berriketa-egunkaria lerroak" Color1="1. kolorea" Color2="2. kolorea" Outline="Eskema" diff --git a/plugins/text-freetype2/data/locale/fi-FI.ini b/plugins/text-freetype2/data/locale/fi-FI.ini index 92bd41c..d95eb45 100644 --- a/plugins/text-freetype2/data/locale/fi-FI.ini +++ b/plugins/text-freetype2/data/locale/fi-FI.ini @@ -3,7 +3,8 @@ Font="Kirjasin" Text="Teksti" TextFile="Tekstitiedosto (UTF-8 tai UTF-16)" TextFileFilter="Tekstitiedostot (*.txt);;" -ChatLogMode="Keskustelun lokitila (6 viimeistä riviä)" +ChatLogMode="Chatlog-tila" +ChatLogLines="Chatlog rivit" Color1="Väri 1" Color2="Väri 2" Outline="Reunaviiva" diff --git a/plugins/text-freetype2/data/locale/fil-PH.ini b/plugins/text-freetype2/data/locale/fil-PH.ini index c5e0686..b279503 100644 --- a/plugins/text-freetype2/data/locale/fil-PH.ini +++ b/plugins/text-freetype2/data/locale/fil-PH.ini @@ -3,7 +3,6 @@ Font="Benditahan" Text="Teksto" TextFile="Pagkikil sa Teksto (UTF-8 o UTF-16)" TextFileFilter="Pakikil sa mga Teksto (*. txt);;" -ChatLogMode="Ang Satsatan ay nasa paraan na log (huling anim na mga linya)" Color1="Kulay isa" Color2="Kulay pangalawa" Outline="Balangkas" diff --git a/plugins/text-freetype2/data/locale/fr-FR.ini b/plugins/text-freetype2/data/locale/fr-FR.ini index b1ae833..9960695 100644 --- a/plugins/text-freetype2/data/locale/fr-FR.ini +++ b/plugins/text-freetype2/data/locale/fr-FR.ini @@ -3,7 +3,8 @@ Font="Police" Text="Texte" TextFile="Fichier texte (UTF-8 ou UTF-16)" TextFileFilter="Fichier texte (*.txt);;" -ChatLogMode="Mode log de chat (6 dernières lignes)" +ChatLogMode="Mode discussion" +ChatLogLines="Lignes à afficher en mode discussion" Color1="Couleur 1" Color2="Couleur 2" Outline="Contour" diff --git a/plugins/text-freetype2/data/locale/gd-GB.ini b/plugins/text-freetype2/data/locale/gd-GB.ini new file mode 100644 index 0000000..9b78ab1 --- /dev/null +++ b/plugins/text-freetype2/data/locale/gd-GB.ini @@ -0,0 +1,4 @@ +Font="Cruth-clò" +Text="Teacsa" +Outline="Oir-loidhne" + diff --git a/plugins/text-freetype2/data/locale/gl-ES.ini b/plugins/text-freetype2/data/locale/gl-ES.ini index 53b20b7..30f20c8 100644 --- a/plugins/text-freetype2/data/locale/gl-ES.ini +++ b/plugins/text-freetype2/data/locale/gl-ES.ini @@ -3,7 +3,6 @@ Font="Fonte" Text="Texto" TextFile="Ficheiro de texto (UTF-8 ou UTF-16)" TextFileFilter="Ficheiros de texto (*.txt);;" -ChatLogMode="Modo de rexistro do chat (últimas 6 liñas)" Color1="Cor 1" Color2="Cor 2" Outline="Contorno" diff --git a/plugins/text-freetype2/data/locale/he-IL.ini b/plugins/text-freetype2/data/locale/he-IL.ini index 8dfc796..656c9bb 100644 --- a/plugins/text-freetype2/data/locale/he-IL.ini +++ b/plugins/text-freetype2/data/locale/he-IL.ini @@ -3,7 +3,6 @@ Font="גופן" Text="טקסט" TextFile="קובץ טקסט (UTF-8 או UTF-16)" TextFileFilter="קבצי טקסט (*.txt);;" -ChatLogMode="מצב יומן צ'אט (6 שורות אחרונות)" Color1="צבע 1" Color2="צבע 2" Outline="קו מתאר" diff --git a/plugins/text-freetype2/data/locale/hr-HR.ini b/plugins/text-freetype2/data/locale/hr-HR.ini index a301f27..99cfe1f 100644 --- a/plugins/text-freetype2/data/locale/hr-HR.ini +++ b/plugins/text-freetype2/data/locale/hr-HR.ini @@ -3,7 +3,6 @@ Font="Font" Text="Tekst" TextFile="Tekstualni dokument (UTF-8 ili UTF-16)" TextFileFilter="Tekstualne datoteke (*.txt);;" -ChatLogMode="Režim ćaskanja (poslednjih 6 redova)" Color1="Boja 1" Color2="Boja 2" Outline="Ivice" diff --git a/plugins/text-freetype2/data/locale/hu-HU.ini b/plugins/text-freetype2/data/locale/hu-HU.ini index ecf0c1b..7a20e13 100644 --- a/plugins/text-freetype2/data/locale/hu-HU.ini +++ b/plugins/text-freetype2/data/locale/hu-HU.ini @@ -3,7 +3,8 @@ Font="Betűtípus" Text="Szöveg" TextFile="Szövegfájl (UTF-8 vagy UTF-16)" TextFileFilter="Szövegfájlok (*.txt);;" -ChatLogMode="Beszélgetésnapló mód (utolsó 6 sor)" +ChatLogMode="Csevegőnapló mód" +ChatLogLines="Csevegőnapló sorok" Color1="Szín 1" Color2="Szín 2" Outline="Körvonal" diff --git a/plugins/text-freetype2/data/locale/it-IT.ini b/plugins/text-freetype2/data/locale/it-IT.ini index f0c919b..fd54ce2 100644 --- a/plugins/text-freetype2/data/locale/it-IT.ini +++ b/plugins/text-freetype2/data/locale/it-IT.ini @@ -3,7 +3,8 @@ Font="Carattere" Text="Testo" TextFile="File di testo (UTF-8 o UTF-16)" TextFileFilter="File di testo (*.txt);;" -ChatLogMode="Modalità Chat log (ultime 6 linee)" +ChatLogMode="Modalità di chat log" +ChatLogLines="Modalità di chat righe" Color1="Colore 1" Color2="Colore 2" Outline="Contorno linea" diff --git a/plugins/text-freetype2/data/locale/ja-JP.ini b/plugins/text-freetype2/data/locale/ja-JP.ini index 4bc35f2..b0e0c18 100644 --- a/plugins/text-freetype2/data/locale/ja-JP.ini +++ b/plugins/text-freetype2/data/locale/ja-JP.ini @@ -3,7 +3,8 @@ Font="フォント" Text="テキスト" TextFile="テキストファイル (UTF-8 または UTF-16)" TextFileFilter="テキストファイル (*.txt);;" -ChatLogMode="チャットログモード (最新6行)" +ChatLogMode="チャットログモード" +ChatLogLines="チャットログ行数" Color1="色 1" Color2="色 2" Outline="輪郭" diff --git a/plugins/text-freetype2/data/locale/ka-GE.ini b/plugins/text-freetype2/data/locale/ka-GE.ini new file mode 100644 index 0000000..ae96763 --- /dev/null +++ b/plugins/text-freetype2/data/locale/ka-GE.ini @@ -0,0 +1,15 @@ +TextFreetype2="ტექსტი (FreeType 2)" +Font="შრიფტი" +Text="ტექსტი" +TextFile="ტექსტური ფაილი (UTF-8 ან UTF-16)" +TextFileFilter="ტექსტური ფაილები (*.txt);;" +ChatLogMode="სასაუბროს (Chat log) რეჟიმი" +ChatLogLines="სასაუბროს ხაზები" +Color1="ფერი 1" +Color2="ფერი 2" +Outline="გარემოხაზულობა" +DropShadow="დაჩრდილვა" +ReadFromFile="ფაილიდან წაკითხვა" +CustomWidth="ტექსტის მითითებული სიგანე" +WordWrap="სიტყვების გადატანა" + diff --git a/plugins/text-freetype2/data/locale/ko-KR.ini b/plugins/text-freetype2/data/locale/ko-KR.ini index b52e35a..d8db482 100644 --- a/plugins/text-freetype2/data/locale/ko-KR.ini +++ b/plugins/text-freetype2/data/locale/ko-KR.ini @@ -3,7 +3,8 @@ Font="글꼴" Text="텍스트" TextFile="텍스트 파일(UTF-8 혹은 UTF-16)" TextFileFilter="텍스트 파일 (*.txt);;" -ChatLogMode="대화 기록 시작 (최근 여섯 줄만 표시)" +ChatLogMode="대화 기록 방식" +ChatLogLines="대화 기록 줄" Color1="색상 1" Color2="색상 2" Outline="외곽선" diff --git a/plugins/text-freetype2/data/locale/ms-MY.ini b/plugins/text-freetype2/data/locale/ms-MY.ini index 95ab97f..b89c658 100644 --- a/plugins/text-freetype2/data/locale/ms-MY.ini +++ b/plugins/text-freetype2/data/locale/ms-MY.ini @@ -3,7 +3,6 @@ Font="Jenis Tulisan" Text="Teks" TextFile="Fail teks (UTF-8 atau UTF-16)" TextFileFilter="Fail teks (*.txt);;" -ChatLogMode="Mod log chat (6 baris terakhir)" Color1="Warna 1" Color2="Warna 2" diff --git a/plugins/text-freetype2/data/locale/nb-NO.ini b/plugins/text-freetype2/data/locale/nb-NO.ini index edc0d3f..539cd56 100644 --- a/plugins/text-freetype2/data/locale/nb-NO.ini +++ b/plugins/text-freetype2/data/locale/nb-NO.ini @@ -3,7 +3,8 @@ Font="Skrifttype" Text="Tekst" TextFile="Tekstfil (UTF-8 eller UTF-16)" TextFileFilter="Tekstfiler (*.txt);;" -ChatLogMode="Chatloggmodus (siste 6 linjer)" +ChatLogMode="Chatloggføringsmodus" +ChatLogLines="Chatloggføringslinjer" Color1="Farge 1" Color2="Farge 2" Outline="Ytterlinje" diff --git a/plugins/text-freetype2/data/locale/nl-NL.ini b/plugins/text-freetype2/data/locale/nl-NL.ini index 2361263..948988a 100644 --- a/plugins/text-freetype2/data/locale/nl-NL.ini +++ b/plugins/text-freetype2/data/locale/nl-NL.ini @@ -3,7 +3,8 @@ Font="Lettertype" Text="Tekst" TextFile="Tekstbestand (UTF-8 of UTF-16)" TextFileFilter="Tekstbestanden (*.txt);;" -ChatLogMode="Chatlog-modus (laatse 6 regels)" +ChatLogMode="Chatlogboek modus" +ChatLogLines="Chatlogboek lijnen" Color1="Kleur 1" Color2="Kleur 2" Outline="Omlijning" diff --git a/plugins/text-freetype2/data/locale/pl-PL.ini b/plugins/text-freetype2/data/locale/pl-PL.ini index 60a7845..7623b20 100644 --- a/plugins/text-freetype2/data/locale/pl-PL.ini +++ b/plugins/text-freetype2/data/locale/pl-PL.ini @@ -3,7 +3,8 @@ Font="Czcionka" Text="Tekst" TextFile="Plik tekstowy (UTF-8 lub UTF-16)" TextFileFilter="Pliki tekstowe (*.txt);;" -ChatLogMode="Tryb dziennika czatu (ostatnie 6 linii)" +ChatLogMode="Tryb czatu" +ChatLogLines="Liczba linii czatu" Color1="Kolor 1" Color2="Kolor 2" Outline="Zarys linii" diff --git a/plugins/text-freetype2/data/locale/pt-BR.ini b/plugins/text-freetype2/data/locale/pt-BR.ini index 483b6c4..d94c465 100644 --- a/plugins/text-freetype2/data/locale/pt-BR.ini +++ b/plugins/text-freetype2/data/locale/pt-BR.ini @@ -3,7 +3,8 @@ Font="Fonte" Text="Texto" TextFile="Arquivo de Texto (UTF-8 ou UTF-16)" TextFileFilter="Arquivos de texto (*.txt);;" -ChatLogMode="Modo de Log de Mensagens (últimas 6 linhas)" +ChatLogMode="Modo de Bate-Papo" +ChatLogLines="Linhas de Bate-Papo" Color1="Cor 1" Color2="Cor 2" Outline="Contorno" diff --git a/plugins/text-freetype2/data/locale/pt-PT.ini b/plugins/text-freetype2/data/locale/pt-PT.ini index 5969cdb..e30102f 100644 --- a/plugins/text-freetype2/data/locale/pt-PT.ini +++ b/plugins/text-freetype2/data/locale/pt-PT.ini @@ -3,7 +3,6 @@ Font="Tipo de letra" Text="Texto" TextFile="Ficheiro de texto (UTF-8 ou UTF-16)" TextFileFilter="Ficheiros de texto (*.txt);;" -ChatLogMode="Modo de registo do chat (últimas 6 linhas)" Color1="Cor 1" Color2="Cor 2" Outline="Contorno" diff --git a/plugins/text-freetype2/data/locale/ro-RO.ini b/plugins/text-freetype2/data/locale/ro-RO.ini index 80dd948..a9a0b55 100644 --- a/plugins/text-freetype2/data/locale/ro-RO.ini +++ b/plugins/text-freetype2/data/locale/ro-RO.ini @@ -3,7 +3,6 @@ Font="Font" Text="Text" TextFile="Fișier text (UTF-8 sau UTF-16)" TextFileFilter="Fișiere text (*.txt);;" -ChatLogMode="Mod de jurnal al chatului (ultimele 6 rânduri)" Color1="Culoarea 1" Color2="Culoarea 2" Outline="Contur" diff --git a/plugins/text-freetype2/data/locale/ru-RU.ini b/plugins/text-freetype2/data/locale/ru-RU.ini index 60d5769..05699cc 100644 --- a/plugins/text-freetype2/data/locale/ru-RU.ini +++ b/plugins/text-freetype2/data/locale/ru-RU.ini @@ -3,7 +3,8 @@ Font="Шрифт" Text="Текст" TextFile="Текстовый файл (UTF-8 или UTF-16)" TextFileFilter="Текстовые файлы (*.txt);;" -ChatLogMode="Режим чат-лога (последние 6 строк)" +ChatLogMode="Режим чат-лога" +ChatLogLines="Кол-во строк чат-лога" Color1="Цвет 1" Color2="Цвет 2" Outline="Обводка" diff --git a/plugins/text-freetype2/data/locale/sk-SK.ini b/plugins/text-freetype2/data/locale/sk-SK.ini index fe45c5d..3ef3bee 100644 --- a/plugins/text-freetype2/data/locale/sk-SK.ini +++ b/plugins/text-freetype2/data/locale/sk-SK.ini @@ -3,7 +3,6 @@ Font="Písmo" Text="Text" TextFile="Textový súbor (UTF-8 alebo UTF-16)" TextFileFilter="Textové súbory (*.txt);;" -ChatLogMode="Režim logovania chatu (posledných 6 riadkov)" Color1="Farba 1" Color2="Farba 2" Outline="Obrys" diff --git a/plugins/text-freetype2/data/locale/sl-SI.ini b/plugins/text-freetype2/data/locale/sl-SI.ini index 1405c5c..297a84d 100644 --- a/plugins/text-freetype2/data/locale/sl-SI.ini +++ b/plugins/text-freetype2/data/locale/sl-SI.ini @@ -2,7 +2,6 @@ Font="Pisava" Text="Tekst" TextFile="Besedilna datoteka (UTF-8 or UTF-16)" TextFileFilter="Besedilne Datoteke (*.txt);;" -ChatLogMode="Način dnevnika klepeta (last 6 lines)" Color1="Barva 1" Color2="Barva 2" Outline="Oris" diff --git a/plugins/text-freetype2/data/locale/sr-CS.ini b/plugins/text-freetype2/data/locale/sr-CS.ini index 1cdf432..04947b3 100644 --- a/plugins/text-freetype2/data/locale/sr-CS.ini +++ b/plugins/text-freetype2/data/locale/sr-CS.ini @@ -3,7 +3,6 @@ Font="Font" Text="Tekst" TextFile="Tekstualni dokument (UTF-8 ili UTF-16)" TextFileFilter="Tekstualne datoteke (*.txt);;" -ChatLogMode="Režim ćaskanja (poslednjih 6 redova)" Color1="Boja 1" Color2="Boja 2" Outline="Ivice" diff --git a/plugins/text-freetype2/data/locale/sr-SP.ini b/plugins/text-freetype2/data/locale/sr-SP.ini index 50b5979..0fad95b 100644 --- a/plugins/text-freetype2/data/locale/sr-SP.ini +++ b/plugins/text-freetype2/data/locale/sr-SP.ini @@ -3,7 +3,6 @@ Font="Фонт" Text="Текст" TextFile="Текстуални документ (UTF-8 или UTF-16)" TextFileFilter="Текстуалне датотеке (*.txt);;" -ChatLogMode="Режим ћаскања (последњих 6 редова)" Color1="Боја 1" Color2="Боја 2" Outline="Ивице" diff --git a/plugins/text-freetype2/data/locale/sv-SE.ini b/plugins/text-freetype2/data/locale/sv-SE.ini index 9d7f819..f0d4776 100644 --- a/plugins/text-freetype2/data/locale/sv-SE.ini +++ b/plugins/text-freetype2/data/locale/sv-SE.ini @@ -3,7 +3,8 @@ Font="Typsnitt" Text="Text" TextFile="Textfil (UTF-8 eller UTF-16)" TextFileFilter="Textfiler (*.txt);;" -ChatLogMode="Chattloggsläge (sista 6 raderna)" +ChatLogMode="Loggningsläge för chatt" +ChatLogLines="Loggrader för chatt" Color1="Färg 1" Color2="Färg 2" Outline="Kontur" diff --git a/plugins/text-freetype2/data/locale/tl-PH.ini b/plugins/text-freetype2/data/locale/tl-PH.ini index ac78f53..848bb38 100644 --- a/plugins/text-freetype2/data/locale/tl-PH.ini +++ b/plugins/text-freetype2/data/locale/tl-PH.ini @@ -3,7 +3,6 @@ Font="Ang font" Text="Teksto" TextFile="Teksto ng file (UTF-8 or UTF-16)" TextFileFilter="Teksto ng mga nilalaman (*.txt);;" -ChatLogMode="Ang log modeng chat (huling 6 na linya)" Color1="Unang Kulay" Color2="Pangalawang Kulay" Outline="Ang Outline" diff --git a/plugins/text-freetype2/data/locale/tr-TR.ini b/plugins/text-freetype2/data/locale/tr-TR.ini index 64ac0e5..31c714a 100644 --- a/plugins/text-freetype2/data/locale/tr-TR.ini +++ b/plugins/text-freetype2/data/locale/tr-TR.ini @@ -3,7 +3,8 @@ Font="Yazı Tipi" Text="Metin" TextFile="Metin Dosyası (UTF-8 veya UTF-16)" TextFileFilter="Metin dosyaları (*.txt);;" -ChatLogMode="Sohbet günlük modu (son 6 satır)" +ChatLogMode="Sohbet günlük modu" +ChatLogLines="Sohbet günlük satırları" Color1="Renk 1" Color2="Renk 2" Outline="Anahat" diff --git a/plugins/text-freetype2/data/locale/uk-UA.ini b/plugins/text-freetype2/data/locale/uk-UA.ini index 2064bb8..d0533ef 100644 --- a/plugins/text-freetype2/data/locale/uk-UA.ini +++ b/plugins/text-freetype2/data/locale/uk-UA.ini @@ -3,7 +3,8 @@ Font="Шрифт" Text="Текст" TextFile="Текстовий файл (UTF-8 або UTF-16)" TextFileFilter="Текстові файли (*. txt);;" -ChatLogMode="Режим чат-журналу (6 останніх рядків)" +ChatLogMode="Режим чат-журналу" +ChatLogLines="Кількість рядків чат-журналу" Color1="Колір 1" Color2="Колір 2" Outline="Контур" diff --git a/plugins/text-freetype2/data/locale/vi-VN.ini b/plugins/text-freetype2/data/locale/vi-VN.ini index 40f2568..2fe00d3 100644 --- a/plugins/text-freetype2/data/locale/vi-VN.ini +++ b/plugins/text-freetype2/data/locale/vi-VN.ini @@ -3,7 +3,6 @@ Font="Phông chữ" Text="Văn bản" TextFile="Tệp văn bản (UTF-8 hoặc UTF-16)" TextFileFilter="Tệp văn bản (*.txt);;" -ChatLogMode="Chế độ bản ghi chat (cuối 6 dòng)" Color1="Màu 1" Color2="Màu 2" Outline="Phác thảo" diff --git a/plugins/text-freetype2/data/locale/zh-CN.ini b/plugins/text-freetype2/data/locale/zh-CN.ini index d54eef5..612fe29 100644 --- a/plugins/text-freetype2/data/locale/zh-CN.ini +++ b/plugins/text-freetype2/data/locale/zh-CN.ini @@ -3,11 +3,12 @@ Font="字体" Text="文本" TextFile="文本文件(UTF-8 或 UTF-16)" TextFileFilter="文本文件(*.txt);;" -ChatLogMode="聊天日志模式 (最后 6 行)" +ChatLogMode="聊天日志模式" +ChatLogLines="聊天日志行数" Color1="颜色 1" Color2="颜色 2" Outline="描边" -DropShadow="下拉阴影" +DropShadow="阴影" ReadFromFile="从文件读取" CustomWidth="自定义文本宽度" WordWrap="自动换行" diff --git a/plugins/text-freetype2/data/locale/zh-TW.ini b/plugins/text-freetype2/data/locale/zh-TW.ini index cf0815e..a4eaef8 100644 --- a/plugins/text-freetype2/data/locale/zh-TW.ini +++ b/plugins/text-freetype2/data/locale/zh-TW.ini @@ -3,7 +3,8 @@ Font="字型" Text="文字" TextFile="文字檔案 (UTF-8 或 UTF-16)" TextFileFilter="文字檔案 (*.txt);;" -ChatLogMode="聊天記錄模式 (最後 6 行)" +ChatLogMode="聊天記錄模式" +ChatLogLines="聊天記錄行" Color1="顏色 1" Color2="顏色 2" Outline="外框" diff --git a/plugins/text-freetype2/obs-convenience.c b/plugins/text-freetype2/obs-convenience.c index a68d7b8..99aaf9b 100644 --- a/plugins/text-freetype2/obs-convenience.c +++ b/plugins/text-freetype2/obs-convenience.c @@ -51,7 +51,7 @@ gs_vertbuffer_t *create_uv_vbuffer(uint32_t num_verts, bool add_color) { } obs_leave_graphics(); - + return tmp; } diff --git a/plugins/text-freetype2/text-freetype2.c b/plugins/text-freetype2/text-freetype2.c index 6fd8571..68dd655 100644 --- a/plugins/text-freetype2/text-freetype2.c +++ b/plugins/text-freetype2/text-freetype2.c @@ -136,6 +136,9 @@ static obs_properties_t *ft2_source_properties(void *unused) obs_properties_add_bool(props, "log_mode", obs_module_text("ChatLogMode")); + obs_properties_add_int(props, "log_lines", + obs_module_text("ChatLogLines"), 1, 1000, 1); + obs_properties_add_path(props, "text_file", obs_module_text("TextFile"), OBS_PATH_FILE, obs_module_text("TextFileFilter"), NULL); @@ -169,7 +172,7 @@ static void ft2_source_destroy(void *data) FT_Done_Face(srcdata->font_face); srcdata->font_face = NULL; } - + for (uint32_t i = 0; i < num_cache_slots; i++) { if (srcdata->cacheglyphs[i] != NULL) { bfree(srcdata->cacheglyphs[i]); @@ -324,7 +327,12 @@ static void ft2_source_update(void *data, obs_data_t *settings) bool from_file = obs_data_get_bool(settings, "from_file"); bool chat_log_mode = obs_data_get_bool(settings, "log_mode"); + uint32_t log_lines = (uint32_t)obs_data_get_int(settings, "log_lines"); + if (srcdata->log_lines != log_lines) { + srcdata->log_lines = log_lines; + vbuf_needs_update = true; + } srcdata->log_mode = chat_log_mode; if (ft2_lib == NULL) goto error; @@ -381,7 +389,7 @@ static void ft2_source_update(void *data, obs_data_t *settings) goto error; } else { - FT_Set_Pixel_Sizes(srcdata->font_face, 0, srcdata->font_size); + FT_Set_Pixel_Sizes(srcdata->font_face, 0, srcdata->font_size); FT_Select_Charmap(srcdata->font_face, FT_ENCODING_UNICODE); } @@ -468,6 +476,8 @@ static void *ft2_source_create(obs_data_t *settings, obs_source_t *source) obs_data_set_default_int(font_obj, "size", 32); obs_data_set_default_obj(settings, "font", font_obj); + obs_data_set_default_int(settings, "log_lines", 6); + obs_data_set_default_int(settings, "color1", 0xFFFFFFFF); obs_data_set_default_int(settings, "color2", 0xFFFFFFFF); diff --git a/plugins/text-freetype2/text-freetype2.h b/plugins/text-freetype2/text-freetype2.h index df2dee1..a3fd49b 100644 --- a/plugins/text-freetype2/text-freetype2.h +++ b/plugins/text-freetype2/text-freetype2.h @@ -60,6 +60,7 @@ struct ft2_source { gs_effect_t *draw_effect; bool outline_text, drop_shadow; bool log_mode, word_wrap; + uint32_t log_lines; obs_source_t *src; }; diff --git a/plugins/text-freetype2/text-functionality.c b/plugins/text-freetype2/text-functionality.c index 0755c27..36c9721 100644 --- a/plugins/text-freetype2/text-functionality.c +++ b/plugins/text-freetype2/text-functionality.c @@ -404,7 +404,7 @@ void load_text_from_file(struct ft2_source *srcdata, const char *filename) void read_from_end(struct ft2_source *srcdata, const char *filename) { FILE *tmp_file = NULL; - uint32_t filesize = 0, cur_pos = 0; + uint32_t filesize = 0, cur_pos = 0, log_lines = 0; char *tmp_read = NULL; uint16_t value = 0, line_breaks = 0; size_t bytes_read; @@ -428,8 +428,9 @@ void read_from_end(struct ft2_source *srcdata, const char *filename) fseek(tmp_file, 0, SEEK_END); filesize = (uint32_t)ftell(tmp_file); cur_pos = filesize; + log_lines = srcdata->log_lines; - while (line_breaks <= 6 && cur_pos != 0) { + while (line_breaks <= log_lines && cur_pos != 0) { if (!utf16) cur_pos--; else cur_pos -= 2; fseek(tmp_file, cur_pos, SEEK_SET); diff --git a/plugins/vlc-video/data/locale/gd-GB.ini b/plugins/vlc-video/data/locale/gd-GB.ini new file mode 100644 index 0000000..74969f9 --- /dev/null +++ b/plugins/vlc-video/data/locale/gd-GB.ini @@ -0,0 +1,7 @@ +Playlist="Liosta-chluiche" +PlayPause="Cluich/Cuir ’na stad" +Restart="Ath-thòisich" +Stop="Cuir stad air" +PlaylistNext="Air adhart" +PlaylistPrev="Air ais" + diff --git a/plugins/vlc-video/data/locale/ka-GE.ini b/plugins/vlc-video/data/locale/ka-GE.ini new file mode 100644 index 0000000..3f39162 --- /dev/null +++ b/plugins/vlc-video/data/locale/ka-GE.ini @@ -0,0 +1,15 @@ +VLCSource="VLC ვიდეო-წყარო" +Playlist="დასაკრავი სია" +LoopPlaylist="დასაკრავი სიის გამეორება დაუსრულებლად" +Shuffle="დასაკრავი სიის არევა" +PlaybackBehavior="მოქმედება ხილვადობის ცვლილებისას" +PlaybackBehavior.StopRestart="შეწყვეტა თუ უხილავია, თავიდან გაშვება თუ ხილულია" +PlaybackBehavior.PauseUnpause="შეჩერება თუ უხილავია, გაშვება თუ ხილულია" +PlaybackBehavior.AlwaysPlay="ყოველთვის გაშვება, ხილვადობის მიუხედავად" +NetworkCaching="ქსელის ბუფერი (მწ)" +PlayPause="გაშვება/შეჩერება" +Restart="თავიდან გაშვება" +Stop="შეწყვეტა" +PlaylistNext="შემდეგი" +PlaylistPrev="წინა" + diff --git a/plugins/vlc-video/vlc-video-plugin.c b/plugins/vlc-video/vlc-video-plugin.c index 4160f91..d87ae51 100644 --- a/plugins/vlc-video/vlc-video-plugin.c +++ b/plugins/vlc-video/vlc-video-plugin.c @@ -57,6 +57,10 @@ LIBVLC_MEDIA_LIST_PLAYER_NEXT libvlc_media_list_player_next_; LIBVLC_MEDIA_LIST_PLAYER_PREVIOUS libvlc_media_list_player_previous_; void *libvlc_module = NULL; +#ifdef __APPLE__ +void *libvlc_core_module = NULL; +#endif + libvlc_instance_t *libvlc = NULL; uint64_t time_start = 0; @@ -154,8 +158,14 @@ static bool load_libvlc_module(void) #ifdef __APPLE__ #define LIBVLC_DIR "/Applications/VLC.app/Contents/MacOS/" +/* According to otoolo -L, this is what libvlc.dylib wants. */ +#define LIBVLC_CORE_FILE LIBVLC_DIR "lib/libvlccore.dylib" #define LIBVLC_FILE LIBVLC_DIR "lib/libvlc.5.dylib" setenv("VLC_PLUGIN_PATH", LIBVLC_DIR "plugins", false); + libvlc_core_module = os_dlopen(LIBVLC_CORE_FILE); + + if (!libvlc_core_module) + return false; #else #define LIBVLC_FILE "libvlc.so.5" #endif @@ -204,6 +214,10 @@ void obs_module_unload(void) { if (libvlc) libvlc_release_(libvlc); +#ifdef __APPLE__ + if (libvlc_core_module) + os_dlclose(libvlc_core_module); +#endif if (libvlc_module) os_dlclose(libvlc_module); }