├── packaging └── debian │ ├── source │ └── format │ ├── changelog │ ├── control │ ├── rules │ └── copyright ├── ci ├── ci_includes.cmd.in ├── forum-update-info.json ├── ci_includes.sh.in ├── macos │ ├── install-pango-x86_64.sh │ ├── install-pango-arm64.sh │ ├── install-packagesbuild.sh │ ├── test-dylib.sh │ ├── change-rpath.sh │ └── archive-brew.py ├── plugin.spec └── windows │ └── package-windows.cmd ├── .gitignore ├── .github ├── containers │ ├── fedora-template │ │ ├── build.sh │ │ └── Dockerfile │ └── fedora-common │ │ └── build.sh └── workflows │ ├── clang-format.yml │ ├── docker-build.yml │ └── main.yml ├── src ├── plugin-macros.h.in ├── obs-text-pthread.h ├── obs-text-pthread-thread.c └── obs-text-pthread-main.c ├── data ├── locale │ └── en-US.ini └── textalpha.effect ├── cmake ├── bundle │ └── macos │ │ ├── entitlements.plist │ │ └── Plugin-Info.plist.in ├── FindPangoft2.cmake ├── FindPangowin32.cmake ├── FindPangocairo.cmake ├── FindGLib.cmake ├── FindCairo.cmake ├── FindPango.cmake └── ObsPluginHelpers.cmake ├── README.md ├── installer ├── installer-Windows.iss.in └── installer-macOS.pkgproj.in ├── CMakeLists.txt ├── .clang-format ├── tools └── list2mlt.py ├── doc └── properties.md └── LICENSE /packaging/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /ci/ci_includes.cmd.in: -------------------------------------------------------------------------------- 1 | set PluginName=@PROJECT_NAME@ 2 | set PluginVersion=@PROJECT_VERSION@ 3 | -------------------------------------------------------------------------------- /ci/forum-update-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin_url": "https://obsproject.com/forum/resources/pthread-text.1287/" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .*.swp 3 | .DS_Store 4 | /build/ 5 | /build32/ 6 | /build64/ 7 | /release/ 8 | /installer/Output/ 9 | 10 | .vscode 11 | .idea 12 | -------------------------------------------------------------------------------- /ci/ci_includes.sh.in: -------------------------------------------------------------------------------- 1 | PLUGIN_NAME="@PROJECT_NAME@" 2 | PLUGIN_VERSION="@PROJECT_VERSION@" 3 | MACOS_BUNDLEID="@MACOS_BUNDLEID@" 4 | LINUX_MAINTAINER_EMAIL="@LINUX_MAINTAINER_EMAIL@" 5 | PKG_SUFFIX='@PKG_SUFFIX@' 6 | -------------------------------------------------------------------------------- /.github/containers/fedora-template/build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -ex 3 | .github/containers/fedora-common/build.sh obs-plugin-build/fedora%releasever% fedora%releasever%-rpmbuild 4 | echo 'FILE_NAME=fedora%releasever%-rpmbuild/*RPMS/**/*.rpm' >> $GITHUB_ENV 5 | -------------------------------------------------------------------------------- /packaging/debian/changelog: -------------------------------------------------------------------------------- 1 | text-pthread-plugin-for-obs (%VERSION%-1~%DISTRIBUTION%) %DISTRIBUTION%; urgency=medium 2 | 3 | [ Norihiro Kamae ] 4 | * Initial release. 5 | 6 | -- Norihiro Kamae (PPA) %DATE% 7 | -------------------------------------------------------------------------------- /ci/macos/install-pango-x86_64.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -ex 4 | 5 | brew install pkg-config 6 | 7 | brew install pango cairo libpng 8 | cp /usr/local/opt/libpng/LICENSE data/LICENSE-libpng 9 | cp /usr/local/opt/pango/COPYING data/COPYING-pango 10 | cp /usr/local/opt/cairo/COPYING data/COPYING-cairo 11 | -------------------------------------------------------------------------------- /src/plugin-macros.h.in: -------------------------------------------------------------------------------- 1 | #ifndef PLUGINNAME_H 2 | #define PLUGINNAME_H 3 | 4 | #define PLUGIN_NAME "@PROJECT_NAME@" 5 | #define PLUGIN_VERSION "@PROJECT_VERSION@" 6 | #cmakedefine PNG_FOUND 7 | 8 | #define blog(level, msg, ...) blog(level, "[" PLUGIN_NAME "] " msg, ##__VA_ARGS__) 9 | 10 | #endif // PLUGINNAME_H 11 | -------------------------------------------------------------------------------- /.github/containers/fedora-template/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fedora:%releasever% 2 | 3 | RUN dnf install -y rpm-build python3-dnf-plugins-core && dnf clean all 4 | RUN dnf install -y obs-studio obs-studio-devel cmake gcc gcc-c++ && dnf clean all 5 | 6 | RUN useradd -s /bin/bash -m rpm 7 | RUN echo >> /etc/sudoers 8 | RUN echo "rpm ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers 9 | 10 | USER rpm 11 | WORKDIR /home/rpm 12 | -------------------------------------------------------------------------------- /ci/macos/install-pango-arm64.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -ex 4 | 5 | brew install pkg-config 6 | removes=(lzo libpng jpeg-turbo) 7 | for pkg in "${removes[@]}"; do 8 | brew uninstall --ignore-dependencies $pkg || true 9 | done 10 | 11 | # The file was generated by the script 'ci/macos/archive-brew.py' 12 | curl -L -o deps.tar.gz http://www.nagater.net/obs-studio/obs-text-pthread-brew-apple-deps-20231231.tar.gz 13 | sha256sum -c <<-EOF 14 | 9a645fd3be53e30b6ab0b7834f1dfc2e444b943039f93ef9f9e5529e1d4d0c65 deps.tar.gz 15 | EOF 16 | (cd / && sudo tar xz) < deps.tar.gz 17 | -------------------------------------------------------------------------------- /data/locale/en-US.ini: -------------------------------------------------------------------------------- 1 | Alignment.Left="Left" 2 | Alignment.Center="Center" 3 | Alignment.Right="Right" 4 | Alignment.Left.Justify="Left Justify" 5 | Alignment.Center.Justify="Center Justify" 6 | Alignment.Right.Justify="Right Justify" 7 | Wrapmode.Word="Word" 8 | Wrapmode.Char="Chararacter" 9 | Wrapmode.WordChar="Word, Character" 10 | Ellipsize.None="None" 11 | Ellipsize.Start="Start" 12 | Ellipsize.Middle="Middle" 13 | Ellipsize.End="End" 14 | Outline.Round="Round" 15 | Outline.Bevel="Bevel" 16 | Outline.Rectangle="Rectangle" 17 | Outline.Sharp="Sharp" 18 | 19 | Alignment.Top="Top" 20 | Alignment.Bottom="Bottom" 21 | -------------------------------------------------------------------------------- /packaging/debian/control: -------------------------------------------------------------------------------- 1 | Source: text-pthread-plugin-for-obs 2 | Section: video 3 | Priority: optional 4 | Maintainer: Norihiro Kamae 5 | Rules-Requires-Root: no 6 | Build-Depends: 7 | debhelper-compat (= 13), 8 | cmake, 9 | obs-studio, 10 | libsimde-dev, 11 | libpango1.0-dev, libpng-dev 12 | Conflicts: obs-text-pthread 13 | Standards-Version: 4.6.2 14 | Homepage: https://github.com/norihiro/obs-text-pthread 15 | 16 | Package: text-pthread-plugin-for-obs 17 | Architecture: any 18 | Multi-Arch: foreign 19 | Depends: 20 | ${shlibs:Depends}, 21 | ${misc:Depends}, 22 | Description: plugin to render text using pango with markups and transitions 23 | 24 | -------------------------------------------------------------------------------- /ci/plugin.spec: -------------------------------------------------------------------------------- 1 | Name: @PLUGIN_NAME_FEDORA@ 2 | Version: @VERSION@ 3 | Release: @RELEASE@%{?dist} 4 | Summary: Text plugin using pango in separated thread for OBS Studio 5 | License: GPLv2+ 6 | 7 | Source0: %{name}-%{version}.tar.bz2 8 | BuildRequires: cmake, gcc, gcc-c++ 9 | BuildRequires: obs-studio-devel 10 | BuildRequires: pango pango-devel 11 | 12 | %description 13 | Text plugin for OBS Studio using pango with mark-up option. 14 | 15 | %prep 16 | %autosetup -p1 17 | 18 | %build 19 | %{cmake} -DLINUX_PORTABLE=OFF -DLINUX_RPATH=OFF 20 | %{cmake_build} 21 | 22 | %install 23 | %{cmake_install} 24 | 25 | %files 26 | %{_libdir}/obs-plugins/@PLUGIN_NAME@.so 27 | %{_datadir}/obs/obs-plugins/@PLUGIN_NAME@/ 28 | -------------------------------------------------------------------------------- /packaging/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | # See debhelper(7) (uncomment to enable). 4 | # Output every command that modifies files on the build system. 5 | #export DH_VERBOSE = 1 6 | 7 | 8 | # See FEATURE AREAS in dpkg-buildflags(1). 9 | #export DEB_BUILD_MAINT_OPTIONS = hardening=+all 10 | 11 | # See ENVIRONMENT in dpkg-buildflags(1). 12 | # Package maintainers to append CFLAGS. 13 | #export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic 14 | # Package maintainers to append LDFLAGS. 15 | #export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed 16 | 17 | 18 | %: 19 | dh $@ 20 | 21 | override_dh_auto_configure: 22 | dh_auto_configure -- \ 23 | -DCMAKE_BUILD_TYPE=Release \ 24 | -DLINUX_PORTABLE=OFF \ 25 | -DQT_VERSION=6 26 | -------------------------------------------------------------------------------- /ci/macos/install-packagesbuild.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | if which packagesbuild; then 6 | exit 0 7 | fi 8 | 9 | packages_url='http://www.nagater.net/obs-studio/Packages.dmg' 10 | packages_hash='6afdd25386295974dad8f078b8f1e41cabebd08e72d970bf92f707c7e48b16c9' 11 | 12 | for ((retry=5; retry>0; retry--)); do 13 | curl -o Packages.dmg $packages_url 14 | sha256sum -c <<<"$packages_hash Packages.dmg" && break 15 | done 16 | 17 | hdiutil attach -noverify Packages.dmg 18 | packages_volume="$(hdiutil info -plist | grep '/Volumes/Packages' | sed 's/.*\(\/Volumes\/[^<]*\)<\/string>/\1/')" 19 | 20 | sudo installer -pkg "${packages_volume}/packages/Packages.pkg" -target / 21 | hdiutil detach "${packages_volume}" 22 | -------------------------------------------------------------------------------- /cmake/bundle/macos/entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.device.camera 8 | 9 | com.apple.security.device.audio-input 10 | 11 | com.apple.security.cs.disable-library-validation 12 | 13 | 14 | com.apple.security.cs.allow-dyld-environment-variables 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /cmake/FindPangoft2.cmake: -------------------------------------------------------------------------------- 1 | find_package(PkgConfig) 2 | pkg_check_modules(PC_PANGOFT2 pangoft2 QUIET) 3 | 4 | FIND_PATH(PANGOFT2_INCLUDE_DIR 5 | NAMES 6 | pango/pangoft2.h 7 | HINTS 8 | ${PC_PANGOFT2_INCLUDEDIR} 9 | ${PC_PANGOFT2_INCLUDE_DIRS} 10 | ) 11 | 12 | FIND_LIBRARY(PANGOFT2_LIBRARY 13 | NAMES 14 | pangoft2 libpangoft2 pangoft2-1.0 15 | HINTS 16 | ${PC_PANGOFT2_LIBDIR} 17 | PATH_SUFFIXES 18 | pango 19 | ) 20 | 21 | include(FindPackageHandleStandardArgs) 22 | find_package_handle_standard_args(PANGOFT2 DEFAULT_MSG 23 | PANGOFT2_LIBRARY PANGOFT2_INCLUDE_DIR 24 | ) 25 | 26 | MARK_AS_ADVANCED(PANGOFT2_INCLUDE_DIR PANGOFT2_LIBRARY) 27 | set(PANGOFT2_INCLUDE_DIRS ${PANGOFT2_INCLUDE_DIR}) 28 | set(PANGOFT2_LIBRARIES ${PANGOFT2_LIBRARY}) 29 | -------------------------------------------------------------------------------- /.github/workflows/clang-format.yml: -------------------------------------------------------------------------------- 1 | name: Clang Format Check 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | clang: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Clang 17 | run: | 18 | sudo apt-get install -y clang-format-18 19 | clang-format -i -fallback-style=none $(git ls-files | grep '\.\(c\|h\)$') 20 | 21 | - name: Check 22 | # Build your program with the given configuration 23 | run: | 24 | dirty=$(git ls-files --modified) 25 | set +x 26 | if [[ $dirty ]]; then 27 | git diff 28 | echo "Error: File(s) are not properly formatted." 29 | echo "$dirty" 30 | exit 1 31 | fi 32 | -------------------------------------------------------------------------------- /data/textalpha.effect: -------------------------------------------------------------------------------- 1 | uniform float4x4 ViewProj; 2 | uniform texture2d image; 3 | uniform float alpha = 1.0; 4 | 5 | sampler_state def_sampler { 6 | Filter = Linear; 7 | AddressU = Clamp; 8 | AddressV = Clamp; 9 | }; 10 | 11 | struct VertInOut { 12 | float4 pos : POSITION; 13 | float2 uv : TEXCOORD0; 14 | }; 15 | 16 | VertInOut VSDefault(VertInOut vert_in) 17 | { 18 | VertInOut vert_out; 19 | vert_out.pos = mul(float4(vert_in.pos.xyz, 1.0), ViewProj); 20 | vert_out.uv = vert_in.uv; 21 | return vert_out; 22 | } 23 | 24 | float4 PSDrawAlpha(VertInOut vert_in) : TARGET 25 | { 26 | float4 rgba = image.Sample(def_sampler, vert_in.uv); 27 | return rgba * alpha; 28 | } 29 | 30 | technique Draw 31 | { 32 | pass 33 | { 34 | vertex_shader = VSDefault(vert_in); 35 | pixel_shader = PSDrawAlpha(vert_in); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ci/windows/package-windows.cmd: -------------------------------------------------------------------------------- 1 | call "build\ci\ci_includes.generated.cmd" 2 | 3 | mkdir package 4 | cd package 5 | 6 | git describe --tags --always > package-version.txt 7 | set /p PackageVersion= 2 | 3 | 4 | 5 | CFBundleName 6 | ${MACOSX_PLUGIN_BUNDLE_NAME} 7 | CFBundleIdentifier 8 | ${MACOSX_PLUGIN_GUI_IDENTIFIER} 9 | CFBundleVersion 10 | ${MACOSX_PLUGIN_BUNDLE_VERSION} 11 | CFBundleShortVersionString 12 | ${MACOSX_PLUGIN_SHORT_VERSION_STRING} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleExecutable 16 | ${MACOSX_PLUGIN_EXECUTABLE_NAME} 17 | CFBundlePackageType 18 | ${MACOSX_PLUGIN_BUNDLE_TYPE} 19 | CFBundleSupportedPlatforms 20 | 21 | MacOSX 22 | 23 | LSMinimumSystemVersion 24 | 10.13 25 | 26 | 27 | -------------------------------------------------------------------------------- /cmake/FindPangocairo.cmake: -------------------------------------------------------------------------------- 1 | find_package(PkgConfig) 2 | pkg_check_modules(PC_PANGOCAIRO pangocairo QUIET) 3 | 4 | FIND_PATH(PANGOCAIRO_INCLUDE_DIR 5 | NAMES 6 | pango/pangocairo.h 7 | HINTS 8 | ${PC_PANGOCAIRO_INCLUDEDIR} 9 | ${PC_PANGOCAIRO_INCLUDE_DIRS} 10 | ) 11 | 12 | FIND_LIBRARY(PANGOCAIRO_LIBRARY 13 | NAMES 14 | pangocairo libpangocairo pangocairo-1.0 15 | HINTS 16 | ${PC_PANGOCAIRO_LIBDIR} 17 | PATH_SUFFIXES 18 | pango 19 | ) 20 | 21 | # include(CMakeFindDependencyMacro) 22 | # find_dependency(cairo) 23 | # list(APPEND PANGOCAIRO_INCLUDE_DIR ${CAIRO_INCLUDE_DIRS}) 24 | # list(APPEND PANGOCAIRO_LIBRARY ${CAIRO_LIBRARIES}) 25 | 26 | include(FindPackageHandleStandardArgs) 27 | find_package_handle_standard_args(PANGOCAIRO DEFAULT_MSG 28 | PANGOCAIRO_LIBRARY PANGOCAIRO_INCLUDE_DIR 29 | ) 30 | 31 | MARK_AS_ADVANCED(PANGOCAIRO_INCLUDE_DIR PANGOCAIRO_LIBRARY) 32 | set(PANGOCAIRO_INCLUDE_DIRS ${PANGOCAIRO_INCLUDE_DIR}) 33 | set(PANGOCAIRO_LIBRARIES ${PANGOCAIRO_LIBRARY}) 34 | -------------------------------------------------------------------------------- /ci/macos/test-dylib.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | obs=28 6 | 7 | function test_dylib 8 | { 9 | echo "$1" >> $2 10 | if ! test -e "$1"; then 11 | echo "Error: File $1 not found." >&2 12 | return 1 13 | fi 14 | loader_path="$(dirname "$1")" 15 | otool -L "$1" | 16 | awk -v loader_path="$loader_path" ' 17 | NR>=2 { 18 | dylib = $1 19 | sub("@loader_path", loader_path, dylib) 20 | sub("/lib/../lib/", "/lib/", dylib) 21 | sub("//", "/", dylib) 22 | print dylib 23 | }' | { 24 | ret=0 25 | while read dylib; do 26 | case "$obs-$dylib" in 27 | 2?-/System/*) continue;; 28 | 2?-/usr/lib/*) continue;; # Some libraries are not found on the host for CI. 29 | 27-@rpath/libobs.0.dylib) continue;; 30 | 27-@rpath/libobs-frontend-api.dylib) continue;; 31 | 27-@executable_path/../Frameworks/Qt*.framework/Versions/5/Qt*) continue;; 32 | 28-@rpath/libobs.framework/Versions/A/libobs) continue;; 33 | 28-@rpath/libobs-frontend-api.1.dylib) continue;; 34 | 28-@rpath/Qt*.framework/Versions/A/Qt*) continue;; 35 | esac 36 | if ! grep -qF "$dylib" "$2"; then 37 | if ! test_dylib "$dylib" "$2"; then 38 | echo "Error: File $1 has a dependency error." >&2 39 | ret=1 40 | fi 41 | fi 42 | done 43 | return $ret 44 | } 45 | } 46 | 47 | for i in "$@"; do 48 | case "$i" in 49 | -27) obs=27;; 50 | -28) obs=28;; 51 | *) test_dylib "$i" $(mktemp);; 52 | esac 53 | done 54 | -------------------------------------------------------------------------------- /.github/containers/fedora-common/build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -ex 4 | 5 | docker_image="$1" 6 | rpmbuild="$2" 7 | 8 | PLUGIN_NAME=$(awk '/^project\(/{print gensub(/project\(([^ ()]*).*/, "\\1", 1, $0)}' CMakeLists.txt) 9 | PLUGIN_NAME_FEDORA="$(sed -e 's/^obs-/obs-studio-plugin-/' <<< "$PLUGIN_NAME")" 10 | eval $(git describe --tag --always --long | awk ' 11 | BEGIN { 12 | VERSION="unknown"; 13 | RELEASE=0; 14 | } 15 | { 16 | if (match($0, /^(.*)-([0-9]*)-g[0-9a-f]*$/, aa)) { 17 | VERSION = aa[1] 18 | RELEASE = aa[2] 19 | } 20 | } 21 | END { 22 | VERSION = gensub(/-(alpha|beta|rc)/, "~\\1", 1, VERSION); 23 | gsub(/["'\''-]/, ".", VERSION); 24 | printf("VERSION='\''%s'\'' RELEASE=%d\n", VERSION, RELEASE + 1); 25 | }') 26 | 27 | rm -rf $rpmbuild 28 | mkdir -p $rpmbuild/{BUILD,BUILDROOT,SRPMS,SOURCES,SPECS,RPMS} 29 | rpmbuild="$(cd $rpmbuild && pwd -P)" 30 | chmod a+w $rpmbuild/{BUILD,BUILDROOT,SRPMS,RPMS} 31 | test -x /usr/sbin/selinuxenabled && /usr/sbin/selinuxenabled && chcon -Rt container_file_t $rpmbuild 32 | 33 | # Prepare files 34 | sed \ 35 | -e "s/@PLUGIN_NAME@/$PLUGIN_NAME/g" \ 36 | -e "s/@PLUGIN_NAME_FEDORA@/$PLUGIN_NAME_FEDORA/g" \ 37 | -e "s/@VERSION@/$VERSION/g" \ 38 | -e "s/@RELEASE@/$RELEASE/g" \ 39 | < ci/plugin.spec \ 40 | > $rpmbuild/SPECS/$PLUGIN_NAME_FEDORA.spec 41 | 42 | git archive --format=tar --prefix=$PLUGIN_NAME_FEDORA-$VERSION/ HEAD | bzip2 > $rpmbuild/SOURCES/$PLUGIN_NAME_FEDORA-$VERSION.tar.bz2 43 | 44 | docker run -v $rpmbuild:/home/rpm/rpmbuild $docker_image bash -c " 45 | sudo dnf builddep -y ~/rpmbuild/SPECS/$PLUGIN_NAME_FEDORA.spec && 46 | sudo chown 0:0 ~/rpmbuild/SOURCES/* && 47 | sudo chown 0:0 ~/rpmbuild/SPECS/* && 48 | rpmbuild -ba ~/rpmbuild/SPECS/$PLUGIN_NAME_FEDORA.spec 49 | " 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pthread Text plugin for OBS Studio 2 | 3 | ## Introduction 4 | 5 | This plugin displays text with many advanced features. 6 | 7 | * Markup 8 | * Text alignment 9 | * Left, center, and right 10 | * Justification 11 | * Outline 12 | * Configurable width, color, and opacity 13 | * Configurable shape of corners 14 | * Blur 15 | * Transition 16 | * Fade-in, fade-out, cross-fade 17 | * Slide 18 | * Threaded glyph drawing 19 | * Lower priority to draw glyphs so that other sources and encoders are not impacted 20 | * More frequent polling of the text file 21 | * Automatic line-break supporting East Asian languages 22 | * Saving as PNG files for post production 23 | 24 | ### Markups 25 | 26 | See [the Pango Markup Language](https://docs.gtk.org/Pango/pango_markup.html) 27 | for detailed markup tags available. 28 | 29 | ### Properties 30 | 31 | [List of properties](doc/properties.md) describes all available propeties. 32 | 33 | ### Updating text 34 | 35 | This plugin can set the text by setting or from a text file. 36 | To have transition, it is recommended that the text is updated from a progam, not by typing on the propeties window. 37 | 38 | #### Updating text using obs-websocket 39 | 40 | You can use `SetSourceSettings` request for [obs-websocket](https://github.com/obsproject/obs-websocket/). 41 | Request fields will be as below for example. 42 | ``` 43 | {"inputName": "source-name", "inputSettings": {"text": "your new text"}} 44 | ``` 45 | 46 | #### Updating text file 47 | 48 | This plugin checks these file attributes; inode, mtime, and size. 49 | Recommended flow to update the text is as below. 50 | 1. Set the source file in the property of this plugin. Let's say ```/dev/shm/text.txt``` for example. 51 | 2. Write to a temporary file. 52 | ```your_program > /dev/shm/text.txt~``` 53 | 3. Move the temporary file to the target file. 54 | ```mv /dev/shm/text.txt~ /dev/shm/text.txt``` 55 | This step is atomic so that the plugin won't read the middle state. 56 | 57 | ### Furture plan 58 | 59 | * and feature requests. 60 | -------------------------------------------------------------------------------- /ci/macos/change-rpath.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | libdir='' 4 | obsver='' 5 | 6 | while (($# > 0)); do 7 | case "$1" in 8 | -lib) 9 | libdir="$2" 10 | shift 2;; 11 | -obs) 12 | obsver="$2" 13 | shift 2;; 14 | *) 15 | break ;; 16 | esac 17 | done 18 | 19 | set -e 20 | 21 | function copy_local_dylib 22 | { 23 | local dylib 24 | t=$(mktemp) 25 | otool -L $1 > $t 26 | awk '/^ \/(usr\/local\/(opt|Cellar)|opt\/homebrew)\/.*\.dylib/{print $1}' $t | 27 | while read -r dylib; do 28 | echo "Changing dependency $1 -> $dylib" 29 | local b=$(basename $dylib) 30 | if test ! -e $libdir/$b; then 31 | mkdir -p $libdir 32 | cp $dylib $libdir 33 | chmod +rwx $libdir/$b 34 | install_name_tool -id "@loader_path/$b" $libdir/$b 35 | copy_local_dylib $libdir/$b 36 | fi 37 | install_name_tool -change "$dylib" "@loader_path/../$libdir/$b" $1 38 | done 39 | rm -f "$t" 40 | } 41 | 42 | function change_obs27_libs 43 | { 44 | # obs-frontend-api: 45 | # OBS 27.2 provides only `libobs-frontend-api.dylib`. 46 | # OBS 28.0 will provide `libobs-frontend-api.1.dylib` and `libobs-frontend-api.dylib`. 47 | # libobs: 48 | # Both OBS 27.2 and 28.0 provides `libobs.dylib`, `libobs.0.dylib`, `libobs.framework/Versions/A/libobs`. 49 | 50 | install_name_tool \ 51 | -change @rpath/QtWidgets.framework/Versions/5/QtWidgets \ 52 | @executable_path/../Frameworks/QtWidgets.framework/Versions/5/QtWidgets \ 53 | -change @rpath/QtGui.framework/Versions/5/QtGui \ 54 | @executable_path/../Frameworks/QtGui.framework/Versions/5/QtGui \ 55 | -change @rpath/QtCore.framework/Versions/5/QtCore \ 56 | @executable_path/../Frameworks/QtCore.framework/Versions/5/QtCore \ 57 | -change @rpath/libobs.framework/Versions/A/libobs \ 58 | @rpath/libobs.0.dylib \ 59 | -change @rpath/libobs-frontend-api.0.dylib \ 60 | @rpath/libobs-frontend-api.dylib \ 61 | "$1" 62 | } 63 | 64 | for i in "$@"; do 65 | case "$obsver" in 66 | 27 | 27.*) 67 | change_obs27_libs "$i" 68 | ;; 69 | 28 | 28.*) 70 | : # Not necessary to change dylib paths for OBS 28 71 | ;; 72 | esac 73 | copy_local_dylib "$i" 74 | done 75 | -------------------------------------------------------------------------------- /packaging/debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Source: 3 | Upstream-Name: text-pthread-plugin-for-obs 4 | Upstream-Contact: Norihiro Kamae 5 | 6 | Files: 7 | * 8 | Copyright: 9 | 2020-2024 Norihiro Kamae 10 | License: GPL-2+ 11 | This package is free software; you can redistribute it and/or modify 12 | it under the terms of the GNU General Public License as published by 13 | the Free Software Foundation; either version 2 of the License, or 14 | (at your option) any later version. 15 | . 16 | This package is distributed in the hope that it will be useful, 17 | but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | GNU General Public License for more details. 20 | . 21 | You should have received a copy of the GNU General Public License 22 | along with this program. If not, see 23 | Comment: 24 | On Debian systems, the complete text of the GNU General 25 | Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". 26 | 27 | Files: 28 | debian/* 29 | Copyright: 30 | 2025 Norihiro Kamae 31 | License: GPL-2+ 32 | This package is free software; you can redistribute it and/or modify 33 | it under the terms of the GNU General Public License as published by 34 | the Free Software Foundation; either version 2 of the License, or 35 | (at your option) any later version. 36 | . 37 | This package is distributed in the hope that it will be useful, 38 | but WITHOUT ANY WARRANTY; without even the implied warranty of 39 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 40 | GNU General Public License for more details. 41 | . 42 | You should have received a copy of the GNU General Public License 43 | along with this program. If not, see 44 | Comment: 45 | On Debian systems, the complete text of the GNU General 46 | Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". 47 | -------------------------------------------------------------------------------- /installer/installer-Windows.iss.in: -------------------------------------------------------------------------------- 1 | #define MyAppName "@PROJECT_NAME@" 2 | #define MyAppVersion "@PROJECT_VERSION@" 3 | #define MyAppPublisher "@PLUGIN_AUTHOR@" 4 | #define MyAppURL "@PLUGIN_URL@" 5 | 6 | [Setup] 7 | ; NOTE: The value of AppId uniquely identifies this application. 8 | ; Do not use the same AppId value in installers for other applications. 9 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 10 | AppId={{FE203133-2424-43B3-8B60-F3F280A0CD3B} 11 | AppName={#MyAppName} 12 | AppVersion={#MyAppVersion} 13 | AppPublisher={#MyAppPublisher} 14 | AppPublisherURL={#MyAppURL} 15 | AppSupportURL={#MyAppURL} 16 | AppUpdatesURL={#MyAppURL} 17 | DefaultDirName={code:GetDirName} 18 | DefaultGroupName={#MyAppName} 19 | OutputBaseFilename={#MyAppName}-{#MyAppVersion}-Windows-Installer 20 | Compression=lzma 21 | SolidCompression=yes 22 | 23 | [Languages] 24 | Name: "english"; MessagesFile: "compiler:Default.isl" 25 | 26 | [Files] 27 | Source: "..\release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 28 | Source: "..\LICENSE"; Flags: dontcopy 29 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 30 | 31 | [Icons] 32 | Name: "{group}\{cm:ProgramOnTheWeb,{#MyAppName}}"; Filename: "{#MyAppURL}" 33 | Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" 34 | 35 | [Code] 36 | procedure InitializeWizard(); 37 | var 38 | GPLText: AnsiString; 39 | Page: TOutputMsgMemoWizardPage; 40 | begin 41 | ExtractTemporaryFile('LICENSE'); 42 | LoadStringFromFile(ExpandConstant('{tmp}\LICENSE'), GPLText); 43 | Page := CreateOutputMsgMemoPage(wpWelcome, 44 | 'License Information', 'Please review the license terms before installing {#MyAppName}', 45 | 'Press Page Down to see the rest of the agreement. Once you are aware of your rights, click Next to continue.', 46 | String(GPLText) 47 | ); 48 | end; 49 | 50 | // credit where it's due : 51 | // following function come from https://github.com/Xaymar/obs-studio_amf-encoder-plugin/blob/master/%23Resources/Installer.in.iss#L45 52 | function GetDirName(Value: string): string; 53 | var 54 | InstallPath: string; 55 | begin 56 | // initialize default path, which will be returned when the following registry 57 | // key queries fail due to missing keys or for some different reason 58 | Result := '{pf}\obs-studio'; 59 | // query the first registry value; if this succeeds, return the obtained value 60 | if RegQueryStringValue(HKLM32, 'SOFTWARE\OBS Studio', '', InstallPath) then 61 | Result := InstallPath 62 | end; 63 | 64 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Plugin Build on Docker 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | branches: 8 | - main 9 | tags: 10 | - '*' 11 | pull_request: 12 | paths-ignore: 13 | - '**.md' 14 | branches: 15 | - main 16 | 17 | env: 18 | artifactName: ${{ contains(github.ref_name, '/') && 'docker-artifact' || github.ref_name }}-rpm 19 | 20 | jobs: 21 | docker_build: 22 | runs-on: ubuntu-latest 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | target: 27 | - fedora41 28 | - fedora42 29 | defaults: 30 | run: 31 | shell: bash 32 | env: 33 | target: ${{ matrix.target }} 34 | 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | with: 39 | fetch-depth: 0 40 | submodules: recursive 41 | 42 | - name: Generate container directory 43 | run: | 44 | cp -a .github/containers/fedora-template .github/containers/$target 45 | releasever="$(cut -b 7- <<< "$target")" 46 | sed -i "s/%releasever%/$releasever/g" .github/containers/$target/* 47 | 48 | - name: Restore docker from cache 49 | id: docker-cache 50 | uses: actions/cache/restore@v4 51 | with: 52 | path: ${{ github.workspace }}/docker-cache 53 | key: docker-cache-${{ matrix.target }}-${{ hashFiles(format('.github/containers/{0}/Dockerfile', matrix.target)) }} 54 | 55 | - name: Build environment 56 | if: ${{ steps.docker-cache.outputs.cache-hit != 'true' }} 57 | run: | 58 | docker build -t obs-plugin-build/$target .github/containers/$target 59 | mkdir -p docker-cache 60 | docker save obs-plugin-build/$target | gzip > docker-cache/obs-plugin-build-$target.tar.gz 61 | 62 | - name: Save docker to cache 63 | uses: actions/cache/save@v4 64 | if: ${{ steps.docker-cache.outputs.cache-hit != 'true' }} 65 | with: 66 | path: ${{ github.workspace }}/docker-cache 67 | key: docker-cache-${{ matrix.target }}-${{ hashFiles(format('.github/containers/{0}/Dockerfile', matrix.target)) }} 68 | 69 | - name: Extract cached environment 70 | if: ${{ steps.docker-cache.outputs.cache-hit == 'true' }} 71 | run: | 72 | zcat docker-cache/obs-plugin-build-$target.tar.gz | docker load 73 | 74 | - name: Build package 75 | run: .github/containers/$target/build.sh 76 | 77 | - name: Upload artifact 78 | uses: actions/upload-artifact@v4 79 | with: 80 | name: ${{ env.artifactName }}-${{ matrix.target }} 81 | path: '${{ env.FILE_NAME }}' 82 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | project(obs-text-pthread VERSION 2.1.0 4 | LANGUAGES C 5 | ) 6 | 7 | set(PLUGIN_AUTHOR "Norihiro Kamae") 8 | set(MACOS_BUNDLEID "net.nagater.obs-text-pthread") 9 | set(MACOS_PACKAGE_UUID "652BAFE7-E356-4A3F-9505-F89615E3A44A") 10 | set(MACOS_INSTALLER_UUID "A4F0E91E-D759-472D-81D5-D1B0CE2CC2D8") 11 | set(PLUGIN_URL "https://obsproject.com/forum/resources/pthread-text.1287/") 12 | set(LINUX_MAINTAINER_EMAIL "norihiro@nagater.net") 13 | 14 | option(ENABLE_COVERAGE "Enable coverage option for GCC" OFF) 15 | 16 | # TAKE NOTE: No need to edit things past this point 17 | 18 | # In case you need C++ 19 | set(CMAKE_CXX_STANDARD 11) 20 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 21 | 22 | find_package(PkgConfig) 23 | 24 | if(PKG_CONFIG_FOUND) 25 | pkg_check_modules(PangoCairo REQUIRED IMPORTED_TARGET pangocairo) 26 | pkg_check_modules(PNG QUIET IMPORTED_TARGET libpng) 27 | add_library(PangoCairo ALIAS PkgConfig::PangoCairo) 28 | if(PNG_FOUND) 29 | add_library(PNG ALIAS PkgConfig::PNG) 30 | endif() 31 | else() 32 | find_package(Pango REQUIRED) 33 | find_package(Cairo REQUIRED) 34 | target_link_libraries(PangoCairo INTERFACE Pango::Pango Cairo::Cairo) 35 | endif() 36 | 37 | if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR}) 38 | find_package(libobs REQUIRED) 39 | include(cmake/ObsPluginHelpers.cmake) 40 | endif() 41 | 42 | configure_file( 43 | src/plugin-macros.h.in 44 | plugin-macros.generated.h 45 | ) 46 | 47 | set(PLUGIN_SOURCES 48 | src/obs-text-pthread-main.c 49 | src/obs-text-pthread-thread.c 50 | ) 51 | 52 | add_library(${PROJECT_NAME} MODULE ${PLUGIN_SOURCES}) 53 | 54 | target_link_libraries(${PROJECT_NAME} 55 | OBS::libobs 56 | PangoCairo 57 | ) 58 | 59 | target_include_directories(${PROJECT_NAME} 60 | PRIVATE 61 | ${CMAKE_CURRENT_BINARY_DIR} 62 | ) 63 | 64 | if(PNG_FOUND) 65 | target_link_libraries(${PROJECT_NAME} PNG) 66 | endif() 67 | 68 | if(OS_WINDOWS) 69 | # Enable Multicore Builds and disable FH4 (to not depend on VCRUNTIME140_1.DLL when building with VS2019) 70 | if (MSVC) 71 | add_definitions(/MP /d2FH4-) 72 | endif() 73 | 74 | target_link_libraries(${PROJECT_NAME} OBS::w32-pthreads) 75 | endif() 76 | 77 | if(OS_LINUX) 78 | target_compile_options(${PROJECT_NAME} PRIVATE -g -Wall -Wextra) 79 | target_link_options(${PROJECT_NAME} PRIVATE -Wl,-z,defs) 80 | target_link_libraries(${PROJECT_NAME} m) 81 | 82 | if(ENABLE_COVERAGE) 83 | target_compile_options(${PROJECT_NAME} PRIVATE -coverage) 84 | target_link_options(${PROJECT_NAME} PRIVATE -coverage) 85 | endif() 86 | endif() 87 | 88 | if(OS_MACOS) 89 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++ -fvisibility=default") 90 | 91 | set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "") 92 | set(MACOSX_PLUGIN_GUI_IDENTIFIER "${MACOS_BUNDLEID}") 93 | set(MACOSX_PLUGIN_BUNDLE_VERSION "${PROJECT_VERSION}") 94 | set(MACOSX_PLUGIN_SHORT_VERSION_STRING "1") 95 | endif() 96 | 97 | setup_plugin_target(${PROJECT_NAME}) 98 | 99 | 100 | if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR}) 101 | configure_file( 102 | ci/ci_includes.sh.in 103 | ci/ci_includes.generated.sh 104 | ) 105 | 106 | configure_file( 107 | installer/installer-macOS.pkgproj.in 108 | installer-macOS.generated.pkgproj 109 | ) 110 | 111 | configure_file( 112 | ci/ci_includes.cmd.in 113 | ci/ci_includes.generated.cmd 114 | ) 115 | configure_file( 116 | installer/installer-Windows.iss.in 117 | installer-Windows.generated.iss 118 | ) 119 | endif() 120 | -------------------------------------------------------------------------------- /cmake/FindGLib.cmake: -------------------------------------------------------------------------------- 1 | # FindGLib.cmake 2 | # 3 | # 4 | # CMake support for GLib/GObject/GIO. 5 | # 6 | # License: 7 | # 8 | # Copyright (c) 2016 Evan Nemerson 9 | # 10 | # Permission is hereby granted, free of charge, to any person 11 | # obtaining a copy of this software and associated documentation 12 | # files (the "Software"), to deal in the Software without 13 | # restriction, including without limitation the rights to use, copy, 14 | # modify, merge, publish, distribute, sublicense, and/or sell copies 15 | # of the Software, and to permit persons to whom the Software is 16 | # furnished to do so, subject to the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be 19 | # included in all copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 28 | # DEALINGS IN THE SOFTWARE. 29 | 30 | find_package(PkgConfig) 31 | 32 | if(PKG_CONFIG_FOUND) 33 | pkg_search_module(GLib_PKG glib-2.0) 34 | endif() 35 | 36 | find_library(GLib_LIBRARY glib-2.0 HINTS ${GLib_PKG_LIBRARY_DIRS}) 37 | set(GLib glib-2.0) 38 | 39 | if(GLib_LIBRARY AND NOT GLib_FOUND) 40 | add_library(${GLib} SHARED IMPORTED) 41 | set_property(TARGET ${GLib} PROPERTY IMPORTED_LOCATION "${GLib_LIBRARY}") 42 | set_property(TARGET ${GLib} PROPERTY INTERFACE_COMPILE_OPTIONS "${GLib_PKG_CFLAGS_OTHER}") 43 | 44 | find_path(GLib_INCLUDE_DIRS "glib.h" 45 | HINTS ${GLib_PKG_INCLUDE_DIRS} 46 | PATH_SUFFIXES "glib-2.0") 47 | 48 | get_filename_component(GLib_LIBDIR "${GLib}" DIRECTORY) 49 | find_path(GLib_CONFIG_INCLUDE_DIR "glibconfig.h" 50 | HINTS 51 | ${GLib_LIBDIR} 52 | ${GLib_PKG_INCLUDE_DIRS} 53 | PATHS 54 | "${CMAKE_LIBRARY_PATH}" 55 | PATH_SUFFIXES 56 | "glib-2.0/include" 57 | "glib-2.0") 58 | unset(GLib_LIBDIR) 59 | 60 | if(GLib_CONFIG_INCLUDE_DIR) 61 | file(STRINGS "${GLib_CONFIG_INCLUDE_DIR}/glibconfig.h" GLib_MAJOR_VERSION REGEX "^#define GLIB_MAJOR_VERSION +([0-9]+)") 62 | string(REGEX REPLACE "^#define GLIB_MAJOR_VERSION ([0-9]+)$" "\\1" GLib_MAJOR_VERSION "${GLib_MAJOR_VERSION}") 63 | file(STRINGS "${GLib_CONFIG_INCLUDE_DIR}/glibconfig.h" GLib_MINOR_VERSION REGEX "^#define GLIB_MINOR_VERSION +([0-9]+)") 64 | string(REGEX REPLACE "^#define GLIB_MINOR_VERSION ([0-9]+)$" "\\1" GLib_MINOR_VERSION "${GLib_MINOR_VERSION}") 65 | file(STRINGS "${GLib_CONFIG_INCLUDE_DIR}/glibconfig.h" GLib_MICRO_VERSION REGEX "^#define GLIB_MICRO_VERSION +([0-9]+)") 66 | string(REGEX REPLACE "^#define GLIB_MICRO_VERSION ([0-9]+)$" "\\1" GLib_MICRO_VERSION "${GLib_MICRO_VERSION}") 67 | set(GLib_VERSION "${GLib_MAJOR_VERSION}.${GLib_MINOR_VERSION}.${GLib_MICRO_VERSION}") 68 | unset(GLib_MAJOR_VERSION) 69 | unset(GLib_MINOR_VERSION) 70 | unset(GLib_MICRO_VERSION) 71 | 72 | list(APPEND GLib_INCLUDE_DIRS ${GLib_CONFIG_INCLUDE_DIR}) 73 | set_property(TARGET ${GLib} PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${GLib_INCLUDE_DIRS}") 74 | endif() 75 | endif() 76 | 77 | include(FindPackageHandleStandardArgs) 78 | find_package_handle_standard_args(GLib 79 | REQUIRED_VARS 80 | GLib_LIBRARY 81 | GLib_INCLUDE_DIRS 82 | VERSION_VAR 83 | GLib_VERSION) 84 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | # please use clang-format version 8 or later 2 | 3 | Standard: Cpp11 4 | AccessModifierOffset: -8 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveAssignments: false 7 | AlignConsecutiveDeclarations: false 8 | AlignEscapedNewlines: Left 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | #AllowAllArgumentsOnNextLine: false # requires clang-format 9 12 | #AllowAllConstructorInitializersOnNextLine: false # requires clang-format 9 13 | AllowAllParametersOfDeclarationOnNextLine: false 14 | AllowShortBlocksOnASingleLine: false 15 | AllowShortCaseLabelsOnASingleLine: false 16 | AllowShortFunctionsOnASingleLine: Inline 17 | AllowShortIfStatementsOnASingleLine: false 18 | #AllowShortLambdasOnASingleLine: Inline # requires clang-format 9 19 | AllowShortLoopsOnASingleLine: false 20 | AlwaysBreakAfterDefinitionReturnType: None 21 | AlwaysBreakAfterReturnType: None 22 | AlwaysBreakBeforeMultilineStrings: false 23 | AlwaysBreakTemplateDeclarations: false 24 | BinPackArguments: true 25 | BinPackParameters: true 26 | BraceWrapping: 27 | AfterClass: false 28 | AfterControlStatement: false 29 | AfterEnum: false 30 | AfterFunction: true 31 | AfterNamespace: false 32 | AfterObjCDeclaration: false 33 | AfterStruct: true 34 | AfterUnion: false 35 | AfterExternBlock: false 36 | BeforeCatch: false 37 | BeforeElse: true 38 | IndentBraces: false 39 | SplitEmptyFunction: true 40 | SplitEmptyRecord: true 41 | SplitEmptyNamespace: true 42 | BreakBeforeBinaryOperators: None 43 | BreakBeforeBraces: Custom 44 | BreakBeforeTernaryOperators: true 45 | BreakConstructorInitializers: BeforeColon 46 | BreakStringLiterals: false # apparently unpredictable 47 | ColumnLimit: 120 48 | CompactNamespaces: false 49 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 50 | ConstructorInitializerIndentWidth: 8 51 | ContinuationIndentWidth: 8 52 | Cpp11BracedListStyle: true 53 | DerivePointerAlignment: false 54 | DisableFormat: false 55 | FixNamespaceComments: false 56 | ForEachMacros: 57 | - 'json_object_foreach' 58 | - 'json_object_foreach_safe' 59 | - 'json_array_foreach' 60 | IncludeBlocks: Preserve 61 | IndentCaseLabels: false 62 | IndentPPDirectives: None 63 | IndentWidth: 8 64 | IndentWrappedFunctionNames: false 65 | KeepEmptyLinesAtTheStartOfBlocks: true 66 | MaxEmptyLinesToKeep: 1 67 | NamespaceIndentation: None 68 | #ObjCBinPackProtocolList: Auto # requires clang-format 7 69 | ObjCBlockIndentWidth: 8 70 | ObjCSpaceAfterProperty: true 71 | ObjCSpaceBeforeProtocolList: true 72 | 73 | PenaltyBreakAssignment: 10 74 | PenaltyBreakBeforeFirstCallParameter: 30 75 | PenaltyBreakComment: 10 76 | PenaltyBreakFirstLessLess: 0 77 | PenaltyBreakString: 10 78 | PenaltyExcessCharacter: 100 79 | PenaltyReturnTypeOnItsOwnLine: 60 80 | 81 | PointerAlignment: Right 82 | ReflowComments: false 83 | SortIncludes: false 84 | SortUsingDeclarations: false 85 | SpaceAfterCStyleCast: false 86 | #SpaceAfterLogicalNot: false # requires clang-format 9 87 | SpaceAfterTemplateKeyword: false 88 | SpaceBeforeAssignmentOperators: true 89 | #SpaceBeforeCtorInitializerColon: true # requires clang-format 7 90 | #SpaceBeforeInheritanceColon: true # requires clang-format 7 91 | SpaceBeforeParens: ControlStatements 92 | #SpaceBeforeRangeBasedForLoopColon: true # requires clang-format 7 93 | SpaceInEmptyParentheses: false 94 | SpacesBeforeTrailingComments: 1 95 | SpacesInAngles: false 96 | SpacesInCStyleCastParentheses: false 97 | SpacesInContainerLiterals: false 98 | SpacesInParentheses: false 99 | SpacesInSquareBrackets: false 100 | #StatementMacros: # requires clang-format 8 101 | # - 'Q_OBJECT' 102 | TabWidth: 8 103 | #TypenameMacros: # requires clang-format 9 104 | # - 'DARRAY' 105 | UseTab: ForContinuationAndIndentation 106 | --- 107 | Language: ObjC 108 | -------------------------------------------------------------------------------- /cmake/FindCairo.cmake: -------------------------------------------------------------------------------- 1 | # FindCairo.cmake 2 | # 3 | # 4 | # CMake support for Cairo. 5 | # 6 | # License: 7 | # 8 | # Copyright (c) 2016 Evan Nemerson 9 | # 10 | # Permission is hereby granted, free of charge, to any person 11 | # obtaining a copy of this software and associated documentation 12 | # files (the "Software"), to deal in the Software without 13 | # restriction, including without limitation the rights to use, copy, 14 | # modify, merge, publish, distribute, sublicense, and/or sell copies 15 | # of the Software, and to permit persons to whom the Software is 16 | # furnished to do so, subject to the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be 19 | # included in all copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 28 | # DEALINGS IN THE SOFTWARE. 29 | 30 | find_package(PkgConfig) 31 | 32 | set(Cairo_DEPS) 33 | 34 | if(PKG_CONFIG_FOUND) 35 | pkg_search_module(Cairo_PKG cairo) 36 | endif() 37 | 38 | find_library(Cairo_LIBRARY cairo HINTS ${Cairo_PKG_LIBRARY_DIRS}) 39 | set(Cairo cairo) 40 | 41 | if(Cairo_LIBRARY) 42 | add_library(${Cairo} SHARED IMPORTED) 43 | set_property(TARGET ${Cairo} PROPERTY IMPORTED_LOCATION "${Cairo_LIBRARY}") 44 | set_property(TARGET ${Cairo} PROPERTY INTERFACE_COMPILE_OPTIONS "${Cairo_PKG_CFLAGS_OTHER}") 45 | 46 | set(Cairo_INCLUDE_DIRS) 47 | 48 | find_path(Cairo_INCLUDE_DIR "cairo.h" 49 | HINTS ${Cairo_PKG_INCLUDE_DIRS}) 50 | 51 | if(Cairo_INCLUDE_DIR) 52 | file(STRINGS "${Cairo_INCLUDE_DIR}/cairo-version.h" Cairo_VERSION_MAJOR REGEX "^#define CAIRO_VERSION_MAJOR +\\(?([0-9]+)\\)?$") 53 | string(REGEX REPLACE "^#define CAIRO_VERSION_MAJOR \\(?([0-9]+)\\)?$" "\\1" Cairo_VERSION_MAJOR "${Cairo_VERSION_MAJOR}") 54 | file(STRINGS "${Cairo_INCLUDE_DIR}/cairo-version.h" Cairo_VERSION_MINOR REGEX "^#define CAIRO_VERSION_MINOR +\\(?([0-9]+)\\)?$") 55 | string(REGEX REPLACE "^#define CAIRO_VERSION_MINOR \\(?([0-9]+)\\)?$" "\\1" Cairo_VERSION_MINOR "${Cairo_VERSION_MINOR}") 56 | file(STRINGS "${Cairo_INCLUDE_DIR}/cairo-version.h" Cairo_VERSION_MICRO REGEX "^#define CAIRO_VERSION_MICRO +\\(?([0-9]+)\\)?$") 57 | string(REGEX REPLACE "^#define CAIRO_VERSION_MICRO \\(?([0-9]+)\\)?$" "\\1" Cairo_VERSION_MICRO "${Cairo_VERSION_MICRO}") 58 | set(Cairo_VERSION "${Cairo_VERSION_MAJOR}.${Cairo_VERSION_MINOR}.${Cairo_VERSION_MICRO}") 59 | unset(Cairo_VERSION_MAJOR) 60 | unset(Cairo_VERSION_MINOR) 61 | unset(Cairo_VERSION_MICRO) 62 | 63 | list(APPEND Cairo_INCLUDE_DIRS ${Cairo_INCLUDE_DIR}) 64 | set_property(TARGET ${Cairo} PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${Cairo_INCLUDE_DIR}") 65 | endif() 66 | endif() 67 | 68 | set(Cairo_DEPS_FOUND_VARS) 69 | foreach(cairo_dep ${Cairo_DEPS}) 70 | find_package(${cairo_dep}) 71 | 72 | list(APPEND Cairo_DEPS_FOUND_VARS "${cairo_dep}_FOUND") 73 | list(APPEND Cairo_INCLUDE_DIRS ${${cairo_dep}_INCLUDE_DIRS}) 74 | 75 | set_property (TARGET ${Cairo} APPEND PROPERTY INTERFACE_LINK_LIBRARIES "${${cairo_dep}}") 76 | endforeach(cairo_dep) 77 | 78 | include(FindPackageHandleStandardArgs) 79 | find_package_handle_standard_args(Cairo 80 | REQUIRED_VARS 81 | Cairo_LIBRARY 82 | Cairo_INCLUDE_DIRS 83 | ${Cairo_DEPS_FOUND_VARS} 84 | VERSION_VAR 85 | Cairo_VERSION) 86 | 87 | unset(Cairo_DEPS_FOUND_VARS) 88 | -------------------------------------------------------------------------------- /cmake/FindPango.cmake: -------------------------------------------------------------------------------- 1 | # FindPango.cmake 2 | # 3 | # 4 | # CMake support for Pango. 5 | # 6 | # License: 7 | # 8 | # Copyright (c) 2016 Evan Nemerson 9 | # 10 | # Permission is hereby granted, free of charge, to any person 11 | # obtaining a copy of this software and associated documentation 12 | # files (the "Software"), to deal in the Software without 13 | # restriction, including without limitation the rights to use, copy, 14 | # modify, merge, publish, distribute, sublicense, and/or sell copies 15 | # of the Software, and to permit persons to whom the Software is 16 | # furnished to do so, subject to the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be 19 | # included in all copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 28 | # DEALINGS IN THE SOFTWARE. 29 | 30 | find_package(PkgConfig) 31 | 32 | set(Pango_DEPS 33 | GLib) 34 | 35 | if(PKG_CONFIG_FOUND) 36 | pkg_search_module(Pango_PKG pango) 37 | message("Pango_PKG_FOUND=${Pango_PKG_FOUND}") 38 | message("Pango_PKG_INCLUDE_DIRS=${Pango_PKG_INCLUDE_DIRS}") 39 | message("Pango_PKG_LIBRARIES=${Pango_PKG_LIBRARIES}") 40 | endif() 41 | 42 | find_library(Pango_LIBRARY pango HINTS ${Pango_PKG_LIBRARY_DIRS}) 43 | set(Pango pango) 44 | 45 | if(Pango_LIBRARY AND NOT Pango_FOUND) 46 | add_library(${Pango} SHARED IMPORTED) 47 | set_property(TARGET ${Pango} PROPERTY IMPORTED_LOCATION "${Pango_LIBRARY}") 48 | set_property(TARGET ${Pango} PROPERTY INTERFACE_COMPILE_OPTIONS "${Pango_PKG_CFLAGS_OTHER}") 49 | 50 | find_path(Pango_INCLUDE_DIR "pango/pango.h" 51 | HINTS ${Pango_PKG_INCLUDE_DIRS}) 52 | 53 | if(Pango_INCLUDE_DIR) 54 | file(STRINGS "${Pango_INCLUDE_DIR}/pango/pango-features.h" Pango_MAJOR_VERSION REGEX "^#define PANGO_VERSION_MAJOR +\\(?([0-9]+)\\)?$") 55 | string(REGEX REPLACE "^#define PANGO_VERSION_MAJOR \\(?([0-9]+)\\)?" "\\1" Pango_MAJOR_VERSION "${Pango_MAJOR_VERSION}") 56 | file(STRINGS "${Pango_INCLUDE_DIR}/pango/pango-features.h" Pango_MINOR_VERSION REGEX "^#define PANGO_VERSION_MINOR +\\(?([0-9]+)\\)?$") 57 | string(REGEX REPLACE "^#define PANGO_VERSION_MINOR \\(?([0-9]+)\\)?" "\\1" Pango_MINOR_VERSION "${Pango_MINOR_VERSION}") 58 | file(STRINGS "${Pango_INCLUDE_DIR}/pango/pango-features.h" Pango_MICRO_VERSION REGEX "^#define PANGO_VERSION_MICRO +\\(?([0-9]+)\\)?$") 59 | string(REGEX REPLACE "^#define PANGO_VERSION_MICRO \\(?([0-9]+)\\)?" "\\1" Pango_MICRO_VERSION "${Pango_MICRO_VERSION}") 60 | set(Pango_VERSION "${Pango_MAJOR_VERSION}.${Pango_MINOR_VERSION}.${Pango_MICRO_VERSION}") 61 | unset(Pango_MAJOR_VERSION) 62 | unset(Pango_MINOR_VERSION) 63 | unset(Pango_MICRO_VERSION) 64 | 65 | list(APPEND Pango_INCLUDE_DIRS ${Pango_INCLUDE_DIR}) 66 | set_property(TARGET ${Pango} PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${Pango_INCLUDE_DIR}") 67 | endif() 68 | endif() 69 | 70 | set(Pango_DEPS_FOUND_VARS) 71 | foreach(pango_dep ${Pango_DEPS}) 72 | find_package(${pango_dep}) 73 | 74 | list(APPEND Pango_DEPS_FOUND_VARS "${pango_dep}_FOUND") 75 | list(APPEND Pango_INCLUDE_DIRS ${${pango_dep}_INCLUDE_DIRS}) 76 | 77 | set_property (TARGET "${Pango}" APPEND PROPERTY INTERFACE_LINK_LIBRARIES "${${pango_dep}}") 78 | endforeach(pango_dep) 79 | 80 | include(FindPackageHandleStandardArgs) 81 | find_package_handle_standard_args(Pango 82 | REQUIRED_VARS 83 | Pango_LIBRARY 84 | Pango_INCLUDE_DIRS 85 | ${Pango_DEPS_FOUND_VARS} 86 | VERSION_VAR 87 | Pango_VERSION) 88 | 89 | unset(Pango_DEPS_FOUND_VARS) 90 | -------------------------------------------------------------------------------- /src/obs-text-pthread.h: -------------------------------------------------------------------------------- 1 | #ifndef OBS_TEXT_PTHREAD_H 2 | #define OBS_TEXT_PTHREAD_H 3 | 4 | #include 5 | 6 | struct tp_texture 7 | { 8 | // data from the thread 9 | uint32_t width, height; 10 | gs_texture_t *tex; 11 | uint8_t *surface; 12 | uint64_t time_ns; 13 | bool config_updated; 14 | bool is_crossfade; 15 | 16 | // internal use in main 17 | uint64_t fadein_start_ns, fadein_end_ns; 18 | uint64_t fadeout_start_ns, fadeout_end_ns; 19 | int fade_alpha; 20 | uint64_t slidein_end_ns, slideout_start_ns; 21 | int slide_u; // >0: slide-out, <0: slide-in 22 | int slide_h; // height during slide 23 | 24 | struct tp_texture *next; 25 | }; 26 | 27 | enum { 28 | ALIGN_LEFT = 0, 29 | ALIGN_CENTER = 1, 30 | ALIGN_RIGHT = 2, 31 | ALIGN_JUSTIFY = 4, 32 | 33 | // for vertical align during transition 34 | ALIGN_TOP = 8, 35 | ALIGN_VCENTER = 16, 36 | ALIGN_BOTTOM = 32, 37 | }; 38 | 39 | enum { 40 | OUTLINE_ROUND = 0, 41 | OUTLINE_BEVEL = 1, 42 | OUTLINE_RECT = 2, 43 | OUTLINE_SHARP = 4, 44 | }; 45 | 46 | struct tp_config 47 | { 48 | char *font_name; 49 | char *font_style; 50 | uint32_t font_size; 51 | uint32_t font_flags; 52 | // TODO: font weight stretch gravity 53 | char *text; 54 | char *text_file; 55 | bool from_file; 56 | bool markup; 57 | uint32_t color; 58 | uint32_t width, height; 59 | bool shrink_size; 60 | uint32_t align; 61 | bool auto_dir; 62 | int wrapmode; 63 | int32_t indent; 64 | int ellipsize; 65 | int spacing; 66 | bool outline; 67 | uint32_t outline_color; 68 | uint32_t outline_width; 69 | uint32_t outline_blur; 70 | uint32_t outline_shape; 71 | bool outline_blur_gaussian; 72 | bool shadow; 73 | uint32_t shadow_color; 74 | int32_t shadow_x, shadow_y; 75 | 76 | uint32_t align_transition; 77 | 78 | uint32_t fadeout_ms, fadein_ms, crossfade_ms; 79 | uint32_t slide_pxps; // pixel/s, or 0 to disable 80 | 81 | #ifdef PNG_FOUND 82 | bool save_file; 83 | char *save_file_dir; 84 | #endif // PNG_FOUND 85 | }; 86 | 87 | struct tp_source 88 | { 89 | // config 90 | // write from main 91 | // read from thread 92 | pthread_mutex_t config_mutex; 93 | struct tp_config config; 94 | bool config_updated; 95 | volatile bool running; 96 | 97 | // new texture 98 | // write from thread 99 | // read from main and set to NULL 100 | pthread_mutex_t tex_mutex; 101 | struct tp_texture *tex_new; 102 | volatile bool text_updating; 103 | 104 | // internal use for main 105 | struct tp_texture *textures; 106 | 107 | // threads 108 | pthread_t thread; 109 | }; 110 | 111 | void tp_thread_start(struct tp_source *src); 112 | void tp_thread_end(struct tp_source *src); 113 | 114 | #define BFREE_IF_NONNULL(x) \ 115 | if (x) { \ 116 | bfree(x); \ 117 | (x) = NULL; \ 118 | } 119 | 120 | static inline void tp_config_destroy_member(struct tp_config *c) 121 | { 122 | BFREE_IF_NONNULL(c->font_name); 123 | BFREE_IF_NONNULL(c->font_style); 124 | BFREE_IF_NONNULL(c->text); 125 | BFREE_IF_NONNULL(c->text_file); 126 | #ifdef PNG_FOUND 127 | BFREE_IF_NONNULL(c->save_file_dir); 128 | #endif // PNG_FOUND 129 | } 130 | 131 | static inline void free_texture(struct tp_texture *t) 132 | { 133 | if (t->tex) { 134 | obs_enter_graphics(); 135 | for (struct tp_texture *i = t; i; i = i->next) { 136 | if (i->tex) 137 | gs_texture_destroy(i->tex); 138 | i->tex = NULL; 139 | } 140 | obs_leave_graphics(); 141 | } 142 | if (t->surface) 143 | bfree(t->surface); 144 | if (t->next) 145 | free_texture(t->next); 146 | bfree(t); 147 | } 148 | 149 | static inline struct tp_texture *pushback_texture(struct tp_texture *dest, struct tp_texture *n) 150 | { 151 | if (!dest) 152 | return n; 153 | for (struct tp_texture *t = dest;; t = t->next) { 154 | if (!t->next) { 155 | t->next = n; 156 | return dest; 157 | } 158 | } 159 | } 160 | 161 | static inline struct tp_texture *popfront_texture(struct tp_texture *t) 162 | { 163 | struct tp_texture *ret = t->next; 164 | t->next = NULL; 165 | free_texture(t); 166 | return ret; 167 | } 168 | 169 | #endif // OBS_TEXT_PTHREAD_H 170 | -------------------------------------------------------------------------------- /ci/macos/archive-brew.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | ''' 3 | This script will do these things. 4 | - List all dependencies in a dylib 5 | - Filter dependencies starting with '/opt/homebrew/' 6 | - Archive the directory of the required libraries. 7 | ''' 8 | 9 | import argparse 10 | import collections 11 | import copy 12 | import os 13 | import re 14 | import sys 15 | import subprocess 16 | 17 | 18 | link_dirs = ( 19 | '/opt/homebrew/lib', 20 | '/opt/homebrew/include', 21 | '/opt/homebrew/bin', 22 | '/opt/homebrew/opt', 23 | '/opt/homebrew/sbin', 24 | '/opt/homebrew/share', 25 | '/opt/homebrew/var', 26 | ) 27 | 28 | def list_dependencies(dylib_name, verbosity=0): 29 | p = subprocess.run(['otool', '-L', dylib_name], stdout=subprocess.PIPE) 30 | ret = [] 31 | for line in p.stdout.decode('ascii').split('\n')[1:]: 32 | line = line.split() 33 | if line: 34 | dep = os.path.realpath(line[0]) 35 | if verbosity > 0: 36 | print(f'{dylib_name} depends {dep}') 37 | ret.append(dep) 38 | return ret 39 | 40 | 41 | def is_brew_dependency(dylib_name): 42 | return dylib_name.startswith('/opt/homebrew/') 43 | 44 | 45 | def dylib_to_dirname(dylib_name): 46 | pp = dylib_name.rsplit(sep='/', maxsplit=2) 47 | if pp[1] != 'lib': 48 | raise ValueError(f'{dylib_name}: The 2nd base name must be "lib"') 49 | return pp[0] 50 | 51 | 52 | def package_files(output_name, dirs): 53 | cmd = ['tar', 54 | '--uid', '0', 55 | '--gid', '0', 56 | '-czf', output_name, 57 | ] 58 | for d in dirs: 59 | cmd.append(d) 60 | subprocess.run(cmd) 61 | 62 | 63 | def find_links(dir_name, cond_func, verbosity): 64 | ret = [] 65 | for root, dirs, files in os.walk(dir_name): 66 | if verbosity > 1: 67 | print(f'root="{root}" dirs="{dirs}" files="{files}"') 68 | for f in files + dirs: 69 | fp = os.path.join(root, f) 70 | if not os.path.islink(fp): 71 | if verbosity > 1: 72 | print(f'{fp} is not a link') 73 | continue 74 | target = os.path.realpath(fp) 75 | if verbosity > 1: 76 | print(f'Checking a link {fp} -> {target} satisfies the condition.') 77 | if cond_func(target): 78 | if verbosity > 0: 79 | print(f'Packaging a link {fp}') 80 | ret.append(fp) 81 | return ret 82 | 83 | 84 | def _main(): 85 | parser = argparse.ArgumentParser( 86 | description='Archive homebrew dependencies of dylib') 87 | parser.add_argument('-o', '--output') 88 | parser.add_argument('-v', '--verbose', action='count', default=0) 89 | parser.add_argument('dylib_name') 90 | args = parser.parse_args() 91 | 92 | deps = set() 93 | queue = collections.deque() 94 | queue.append(args.dylib_name) 95 | while len(queue) > 0: 96 | f = queue.popleft() 97 | aa = list_dependencies(f, verbosity=args.verbose-1) 98 | for a in aa: 99 | if not is_brew_dependency(a): 100 | continue 101 | if a in deps: 102 | continue 103 | deps.add(a) 104 | queue.append(a) 105 | 106 | files = set() 107 | for a in deps: 108 | files.add(dylib_to_dirname(a)) 109 | 110 | dirs = copy.copy(files) 111 | 112 | def _is_packaging(target): 113 | if args.verbose >= 3: 114 | print(f'_is_packaging: target="{target}"') 115 | for d in dirs: 116 | if target.startswith(d): 117 | return True 118 | return False 119 | 120 | for d in link_dirs: 121 | if args.verbose >= 3: 122 | print(f'Finding symlink in "{d}"') 123 | for l in find_links(d, _is_packaging, verbosity=args.verbose-1): 124 | files.add(l) 125 | 126 | if args.output: 127 | package_files(args.output, files) 128 | 129 | 130 | if __name__ == '__main__': 131 | _main() 132 | -------------------------------------------------------------------------------- /tools/list2mlt.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | 3 | import xml.etree.ElementTree as ET 4 | 5 | def ms2str(t): 6 | ms = t%1000; t /= 1000 7 | s = t%60; t /= 60 8 | m = t%60; t /= 60 9 | h = t 10 | return '%d:%02d:%02d.%03d'%(h, m, s, ms) 11 | 12 | _key = 1 13 | class TPItem: 14 | def __init__(self, ms, png, w, h, fadein, crossfade, config): 15 | global _key 16 | self.ms_start = ms 17 | self.png = png 18 | self.w = w 19 | self.h = h 20 | self.ms_end = 0 21 | self.ms_end_fade = 0 22 | self.key = _key; _key += 1 23 | self.fadein = fadein 24 | self.crossfade = crossfade 25 | self.fadeout = False 26 | self.config = config 27 | def __str__(self): 28 | return 'TPItem(%s %s-%s %dx%d)'%(self.png, ms2str(self.ms_start), ms2str(self.ms_end), self.w, self.h) 29 | 30 | def add_property(element, name, value): 31 | ET.SubElement(element, 'property', attrib={'name': name}).text = value 32 | 33 | n_playlist = 0 34 | class Playlist(): 35 | def __init__(self): 36 | global n_playlist 37 | self.key = n_playlist; n_playlist += 1 38 | self.items = list() 39 | def __str__(self): 40 | return 'playlist%d'%self.key 41 | def add_item(self, item): 42 | self.items.append(item) 43 | def ms_end(self): 44 | if len(self.items)<=0: 45 | return 0 46 | return self.items[-1].ms_end_fade 47 | def add_to_mlt(self, mlt): 48 | playlist = ET.SubElement(mlt, 'playlist', attrib={'autoclose': '1', 'id': self.__str__()}) 49 | ms_last = 0 50 | for item in self.items: 51 | if item.ms_start > ms_last: 52 | ET.SubElement(playlist, 'blank', attrib={'length': ms2str(item.ms_start - ms_last)}) 53 | entry = ET.SubElement(playlist, 'entry', attrib={ 54 | 'producer': 'producer%d'%item.key, 55 | 'in': ms2str(0), 56 | 'out': ms2str(item.ms_end_fade - item.ms_start), 57 | }) 58 | ms_last = item.ms_end_fade 59 | f1 = ET.SubElement(entry, 'filter', attrib={'id': 'filter%d'%item.key}) 60 | x = (1920 - item.w)/2 # TODO: x 61 | y = 1060 - item.h # TODO: y 62 | rect_xywh = '%d %d %d %d'%(x, y, item.w, item.h) 63 | rects = list() 64 | fadein_ms = int(item.config['fadein_ms']) 65 | fadeout_ms = int(item.config['fadeout_ms']) 66 | fadeout_ms = int(int(fadeout_ms * 30 / 1000) * 1000 / 30) 67 | if fadein_ms>0 and item.fadein: 68 | rects.append('%s=%s 0.0'%(ms2str(0), rect_xywh)) 69 | rects.append('%s=%s 1.0'%(ms2str(fadein_ms), rect_xywh)) 70 | else: 71 | rects.append('%s=%s 1.0'%(ms2str(0), rect_xywh)) 72 | if fadeout_ms>0 and item.fadeout: 73 | rects.append('%s=%s 1.0'%(ms2str(item.ms_end-item.ms_start), rect_xywh)) 74 | rects.append('%s=%s 0.0'%(ms2str(item.ms_end_fade-item.ms_start), rect_xywh)) 75 | add_property(f1, 'rotate_center', '1') 76 | add_property(f1, 'mlt_service', 'qtblend') 77 | add_property(f1, 'kdenlive_id', 'qtblend') 78 | add_property(f1, 'rect', ';'.join(rects)) 79 | add_property(f1, 'rotation', '00:00:00.000=0') 80 | add_property(f1, 'compositing', '0') 81 | add_property(f1, 'distort', '0') 82 | add_property(f1, 'kdenlive:collapsed', '0') 83 | 84 | class TP2Mlt: 85 | def push(self, item): 86 | self.items.append(item) 87 | def read_list(self, f): 88 | first = True 89 | self.items = list() 90 | item = None 91 | ms_first = None 92 | self.config = dict() 93 | while True: 94 | line = f.readline().strip('\r\n') 95 | if not line: 96 | break 97 | line = line.split('\t') 98 | if len(line) < 2: 99 | continue 100 | if line[0]=='#' and line[1][-1]==':': 101 | self.config = dict(self.config) 102 | self.config[line[1][:-1]] = line[2] 103 | continue 104 | ms = int(line[0]) 105 | ms = int(int(ms*30/1000) * 1000/30) 106 | if not ms_first: 107 | ms_first = ms 108 | ms = 0 109 | else: 110 | ms -= ms_first 111 | png = line[1] 112 | is_crossfade = item and png!='-' 113 | is_fadein = not item and png!='-' 114 | is_fadeout = item and png=='-' 115 | if item: 116 | item.ms_end = ms 117 | if is_fadeout: 118 | item.fadeout = True 119 | self.push(item) 120 | item = None 121 | if png!='-' and len(line)>=4: 122 | item = TPItem(ms, png, int(line[2]), int(line[3]), is_fadein, is_crossfade, self.config) 123 | 124 | def make_xml(self): 125 | kdenlive = True 126 | mlt = ET.Element('mlt', attrib={'version': '6.22'}) 127 | ET.SubElement(mlt, 'profile', attrib={ 128 | 'width': '1920', 129 | 'height': '1080', 130 | 'display_aspect_num': '16', 131 | 'display_aspect_den': '9', 132 | 'frame_rate_num': '30000', 133 | 'frame_rate_den': '1001', 134 | 'sample_aspect_num': '1', 135 | 'sample_aspect_den': '1', 136 | 'colorspace': '709', 137 | 'progressive': '1', 138 | }) 139 | fadeout_ms = 0 140 | for item in self.items: 141 | producer = ET.SubElement(mlt, 'producer', attrib={'id': 'producer%d'%item.key}) 142 | add_property(producer, 'resource', item.png) 143 | add_property(producer, 'mlt_service', 'qimage') 144 | fadeout_ms = int(item.config['fadeout_ms']) 145 | fadeout_ms = int(int(fadeout_ms * 30 / 1000) * 1000 / 30) 146 | if fadeout_ms>0 and item.fadeout: 147 | item.ms_end_fade = item.ms_end + fadeout_ms 148 | else: 149 | item.ms_end_fade = item.ms_end 150 | if kdenlive: 151 | main_bin = ET.SubElement(mlt, 'playlist', attrib={'id': 'main_bin'}) 152 | add_property(main_bin, 'xml_retain', '1') 153 | add_property(main_bin, 'kdenlive:docproperties.version', '1') 154 | for item in self.items: 155 | ET.SubElement(main_bin, 'entry', attrib={ 156 | 'producer': 'producer%d'%item.key, 157 | 'in': ms2str(0), 158 | 'out': ms2str(item.ms_end_fade - item.ms_start), 159 | }) 160 | black_track = ET.SubElement(mlt, 'producer', attrib={ 161 | 'id': 'black_track', 162 | 'in': ms2str(0), 163 | 'out': ms2str(self.items[-1].ms_end_fade + fadeout_ms), 164 | }) 165 | add_property(black_track, 'resource', 'transparent') 166 | add_property(black_track, 'mlt_service', 'color') 167 | playlists = list() 168 | for item in self.items: 169 | i_playlist = len(playlists) 170 | while i_playlist > 0: 171 | if playlists[i_playlist-1].ms_end() > item.ms_start: 172 | break 173 | i_playlist -= 1 174 | if i_playlist >= len(playlists): 175 | playlists.append(Playlist()) 176 | playlists[i_playlist].add_item(item) 177 | for playlist in playlists: 178 | playlist.add_to_mlt(mlt) 179 | # kdenlive requires an enpty playlist at the end of each tractor 180 | playlist_e = Playlist() 181 | playlist_e.add_to_mlt(mlt) 182 | tractor = ET.SubElement(mlt, 'tractor', attrib={'id': 'tractor%d'%playlist.key}) 183 | if kdenlive: 184 | add_property(tractor, 'kdenlive:trackheight', '61') 185 | add_property(tractor, 'kdenlive:timeline_active', '1') 186 | ET.SubElement(tractor, 'track', attrib={'hide': 'audio', 'producer': str(playlist)}) 187 | ET.SubElement(tractor, 'track', attrib={'hide': 'both', 'producer': str(playlist_e)}) 188 | global_feed = ET.SubElement(mlt, 'tractor', attrib={'id': 'tractor%d'%len(playlists), 'global_feed': '1'}) 189 | ET.SubElement(global_feed, 'track', attrib={'producer': 'black_track'}) 190 | for playlist in playlists: 191 | ET.SubElement(global_feed, 'track', attrib={'producer': 'tractor%d'%playlist.key}) 192 | self.mlt = mlt 193 | def pretty(self): 194 | from xml.dom import minidom 195 | return minidom.parseString(ET.tostring(self.mlt)).toprettyxml() 196 | 197 | def main(): 198 | inst = TP2Mlt() 199 | import sys 200 | f = sys.argv[1] 201 | inst.read_list(open(f)) 202 | inst.make_xml() 203 | print(inst.pretty()) 204 | return 0 205 | 206 | if __name__=='__main__': 207 | import sys 208 | sys.exit(main()) 209 | 210 | -------------------------------------------------------------------------------- /doc/properties.md: -------------------------------------------------------------------------------- 1 | # PThread Text Properties 2 | 3 | ## Text Properties 4 | 5 | ### Font 6 | | Name | Key | Type | Range | Default | 7 | | ---- | --- | ---- | ----- | ------- | 8 | | Font | `font` | object | n/a | size=64 | 9 | 10 | This property specifies font for the text. 11 | Still the font can be changed by mark up in the text. 12 | 13 | ### Text 14 | | Name | Key | Type | Range | Default | 15 | | ---- | --- | ---- | ----- | ------- | 16 | | Text | `text` | string | can be multiline | "" | 17 | 18 | This property will be taken if _Read text from a file_ is unchecked. 19 | If _Pango mark-up_ is checked, the text has to have a correct mark-up syntax. 20 | 21 | ### Read text from a file 22 | | Name | Key | Type | Range | Default | 23 | | ---- | --- | ---- | ----- | ------- | 24 | | Read text from a file | `from_file` | bool | false, true | false | 25 | 26 | If checked, the text will be retreived from a file specified by _Text file_ property. 27 | 28 | ### Text file 29 | | Name | Key | Type | Range | Default | 30 | | ---- | --- | ---- | ----- | ------- | 31 | | Text file | `text_file` | string | Absolute file path | | 32 | 33 | This property will be taken if _Read text from a file_ is checked. 34 | If _Pango mark-up_ is checked, the text has to have a correct mark-up syntax. 35 | 36 | ### Pango mark-up 37 | | Name | Key | Type | Range | Default | 38 | | ---- | --- | ---- | ----- | ------- | 39 | | Pango mark-up | `markup` | bool | false, true | true | 40 | 41 | If checked, the text is parsed by pango as a mark-up language. 42 | See [the Pango Markup Language](https://developer.gnome.org/pygtk/stable/pango-markup-language.html) 43 | for details. 44 | 45 | ## Color and Size Properties 46 | 47 | ### Color 48 | | Name | Key | Type | Range | Default | 49 | | ---- | --- | ---- | ----- | ------- | 50 | | Color | `color` | int | 0-4294967295 | #FFFFFFFF (solid white) | 51 | 52 | This property selects the default color of the text. 53 | Still the mark-up can overwrite the color. 54 | Transparent color value is also accepted. 55 | 56 | ### Width and Height 57 | | Name | Key | Type | Range | Default | 58 | | ---- | --- | ---- | ----- | ------- | 59 | | Width | `width` | int | 0-16384 | 1920 | 60 | | Height | `height` | int | 0-16384 | 1080 | 61 | 62 | These properties specify the canvas size when drawing the text. 63 | 64 | ### Automatically shrink size 65 | | Name | Key | Type | Range | Default | 66 | | ---- | --- | ---- | ----- | ------- | 67 | | Automatically shrink size | `shrink_size` | bool | false, true | true | 68 | 69 | If checked, shrinks the canvas size to eliminate empty spaces. 70 | 71 | ## Text Layouting Properties 72 | 73 | ### Alignment 74 | | Name | Key | Type | Range | Default | 75 | | ---- | --- | ---- | ----- | ------- | 76 | | Alignment | `align` | int | 0-6 | 0 (left) | 77 | 78 | This property specifies horizontal alignment and justification. 79 | Justification will happen if the text in a line is too long and need to wrap the text. 80 | From the API, available numbers are shown as below. 81 | 82 | | Value | Description | 83 | | ----- | ----------- | 84 | | 0 | Align to left | 85 | | 1 | Align to center | 86 | | 2 | Align to right | 87 | | 4 | Justification (can be added to one of above values.) | 88 | 89 | ### Calculate the bidirectonal base direction 90 | | Name | Key | Type | Range | Default | 91 | | ---- | --- | ---- | ----- | ------- | 92 | | Calculate the bidirectonal base direction | `auto_dir` | bool | false, true | true | 93 | 94 | When this flag is true (the default), then paragraphs in layout that begin with strong right-to-left characters (Arabic and Hebrew principally), will have right-to-left layout, paragraphs with letters from other scripts will have left-to-right layout. Paragraphs with only neutral characters get their direction from the surrounding paragraphs. 95 | When false, the layout is left-to-right. 96 | 97 | See [`pango_layout_set_auto_dir`](https://developer.gnome.org/pango/1.46/pango-Layout-Objects.html#pango-layout-set-auto-dir) 98 | for detailed description. 99 | 100 | ### Wrap text 101 | | Name | Key | Type | Range | Default | 102 | | ---- | --- | ---- | ----- | ------- | 103 | | Wrap text | `wrapmode` | int | 0-2 | 0 (word) | 104 | 105 | | Value | Selection | Description | 106 | | ----- | --------- | ----------- | 107 | | 0 | Word | wrap lines at word boundaries. | 108 | | 1 | Char | wrap lines at character boundaries. | 109 | | 2 | WordChar | wrap lines at word boundaries, but fall back to character boundaries if there is not enough space for a full word. 110 | 111 | ### Indent 112 | | Name | Key | Type | Range | Default | 113 | | ---- | --- | ---- | ----- | ------- | 114 | | Indent | `indent` | int | -32767 - +32767 | 0 | 115 | 116 | This property sets the width to indent each paragraph. 117 | 118 | ### Ellipsize 119 | | Name | Key | Type | Range | Default | 120 | | ---- | --- | ---- | ----- | ------- | 121 | | Ellipsize | `ellipsize` | int | 0 - 3 | 0 (none) | 122 | 123 | This parameter controls ellipsization, if long line is given, some characters are removed in order to make it fit to the width. 124 | 125 | | Value | Selection | Description | 126 | | 0 | None | No ellipsization. | 127 | | 1 | Start | Omit characters at the start of the text. | 128 | | 2 | Middle | Omit characters in the middle of the text. | 129 | | 3 | End | Omit characters at the end of the text. | 130 | 131 | ### Line spacing 132 | | Name | Key | Type | Range | Default | 133 | | ---- | --- | ---- | ----- | ------- | 134 | | Line spacing | `spacing` | int | -65536 - +65536 | 0 | 135 | 136 | This parameter controls additional spacing between lines. 137 | 138 | ## Decoration Properties 139 | 140 | This plugin provides two decorations; outline and shadow. 141 | The outline makes the text thicker with different color. 142 | The shadow has the same figure as the text and the outline and stays underneath them and drawn by yet another color. 143 | 144 | ### Outline 145 | | Name | Key | Type | Range | Default | 146 | | ---- | --- | ---- | ----- | ------- | 147 | | Outline | `outline` | bool | false, true | false | 148 | | Outline color | `outline_color` | int | | `#FF000000` (black) | 149 | | Outline width | `outline_width` | int | 0 - 65536 | 0 | 150 | | Outline blur | `outline_blur` | int | 0 - 65536 | 0 | 151 | | Outline shape | `outline_shape` | int | | Round | 152 | | Outline blur with gaussian function | `outline_blur_gaussian` | bool | false, true | true | 153 | 154 | The width specifies the width of outline in pixels. 155 | The blur will composite several outline that have different width from `width+blur` to `width-blur`. 156 | You should set non-zero value for at least width or blur. 157 | 158 | Outline shape takes one of these choice. 159 | - Round: (default) 160 | - Bevel: The outline becomes octagonal shape at the corner. 161 | - Rect: The outline becomes rectangle shape at the corner. 162 | - Sharp: The outline becomes pointed sharp at acute corner. The maximum length of the acute corner is limited to 4 since there is no limit if the corner is super acute. Usually it should not reach the limit. 163 | 164 | When enabling blur with gaussian function, outline opacity is controlled by the gaussian cumulative distribution function of the distance from the edge. 165 | This option is deprecated, and will be fixed to true. 166 | 167 | See also [examples](https://github.com/norihiro/obs-text-pthread/wiki/properties#outline-shape). 168 | 169 | ### Shadow 170 | | Name | Key | Type | Range | Default | 171 | | ---- | --- | ---- | ----- | ------- | 172 | | Shadow | `shadow` | bool | false, true | false | 173 | | Shadow color | `shadow_color` | int | | `#FF000000` (black) | 174 | | Shadow offset x | `shadow_x` | int | -65536 - +65536 | 2 | 175 | | Shadow offset y | `shadow_y` | int | -65536 - +65536 | 3 | 176 | 181 | 182 | If enabled, a shadow is drawn underneath the text and the outline. 183 | The color of the shadow can be specified by `Shadow color` property. 184 | The location of the shadow is specified by x and y coordinate. Please note that nothing will be displayed if both x and y are zero. 185 | 186 | ## Transition Properties 187 | 188 | ### Transition Alignment 189 | | Name | Key | Type | Range | Default | 190 | | ---- | --- | ---- | ----- | ------- | 191 | | Transition alignment (horizontal) | `align_transition.h` | int | 0, 1, 2 | 0 (left) | 192 | | Transition alignment (vertical) | `align_transition.v` | int | 8, 16, 32 | 8 (top) | 193 | 194 | This parameter is used if crossfade or slide transition is enabled. 195 | Please set as same as the alignment setting for the scene item. 196 | 197 | ### Fade 198 | | Name | Key | Type | Range | Default | 199 | | ---- | --- | ---- | ----- | ------- | 200 | Fadein time [ms] | `fadein_ms` | int | 0 - 4294 | 0 | 201 | Fadeout time [ms] | `fadeout_ms` | int | 0 - 4294 | 0 | 202 | Crossfade time [ms] | `crossfade_ms` | int | 0 - 4294 | 0 | 203 | 204 | These parameters enable fade-in/out and cross fade if set to non-zero value. 205 | Fadein is applied if the text is set from blank to non-blank. 206 | Fadeout is applied if the text is set from non-blank to blank. 207 | Crossfade is applied if the text is set from non-blank to non-blank. 208 | 209 | ### Slide 210 | | Name | Key | Type | Range | Default | 211 | | ---- | --- | ---- | ----- | ------- | 212 | Slide \[px/s\] | `slide_pxps` | int | 0 - 65500 | 0 | 213 | 214 | This parameter enables slide transition if set to non-zero value. 215 | When the text is updated, old text will slide upward and the new text will slide from the bottom. 216 | 217 | Set 0 to disable slide. Default is 0. 218 | 219 | Slide feature is under development. Behavior might change in the next release. 220 | 221 | ## Post-production Support Properties 222 | 223 | ### Save as PNG 224 | | Name | Key | Type | Range | Default | 225 | | ---- | --- | ---- | ----- | ------- | 226 | Save as PNG | `save_file` | bool | false, true | false | 227 | 228 | Each text will be saved as a PNG file if this property is checked. 229 | The PNG image has alpha channel so that you can import the file to your non-linear video editor and overlay on your video. 230 | 231 | Default is disabled. 232 | 233 | ### Directory to save 234 | | Name | Key | Type | Range | Default | 235 | | ---- | --- | ---- | ----- | ------- | 236 | Directory to save | `save_file_dir` | string | Absolute path to a directory | | 237 | 238 | Specify the directory to save the PNG files. 239 | -------------------------------------------------------------------------------- /cmake/ObsPluginHelpers.cmake: -------------------------------------------------------------------------------- 1 | if(POLICY CMP0087) 2 | cmake_policy(SET CMP0087 NEW) 3 | endif() 4 | 5 | set(OBS_STANDALONE_PLUGIN_DIR ${CMAKE_SOURCE_DIR}/release) 6 | set(INCLUDED_LIBOBS_CMAKE_MODULES ON) 7 | 8 | include(GNUInstallDirs) 9 | if(${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") 10 | set(OS_MACOS ON) 11 | set(OS_POSIX ON) 12 | elseif(${CMAKE_SYSTEM_NAME} MATCHES "Linux|FreeBSD|OpenBSD") 13 | set(OS_POSIX ON) 14 | string(TOUPPER "${CMAKE_SYSTEM_NAME}" _SYSTEM_NAME_U) 15 | set(OS_${_SYSTEM_NAME_U} ON) 16 | elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") 17 | set(OS_WINDOWS ON) 18 | set(OS_POSIX OFF) 19 | endif() 20 | 21 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT AND (OS_WINDOWS OR OS_MACOS)) 22 | set(CMAKE_INSTALL_PREFIX 23 | ${OBS_STANDALONE_PLUGIN_DIR} 24 | CACHE STRING "Directory to install OBS plugin after building" FORCE) 25 | endif() 26 | 27 | if(NOT CMAKE_BUILD_TYPE) 28 | set(CMAKE_BUILD_TYPE 29 | "RelWithDebInfo" 30 | CACHE STRING 31 | "OBS build type [Release, RelWithDebInfo, Debug, MinSizeRel]" FORCE) 32 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Release RelWithDebInfo 33 | Debug MinSizeRel) 34 | endif() 35 | 36 | if(NOT QT_VERSION) 37 | set(QT_VERSION 38 | "5" 39 | CACHE STRING "OBS Qt version [5, 6]" FORCE) 40 | set_property(CACHE QT_VERSION PROPERTY STRINGS 5 6) 41 | endif() 42 | 43 | macro(find_qt) 44 | set(oneValueArgs VERSION) 45 | set(multiValueArgs COMPONENTS COMPONENTS_WIN COMPONENTS_MAC COMPONENTS_LINUX) 46 | cmake_parse_arguments(FIND_QT "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) 47 | 48 | if(OS_WINDOWS) 49 | find_package( 50 | Qt${FIND_QT_VERSION} 51 | COMPONENTS ${FIND_QT_COMPONENTS} ${FIND_QT_COMPONENTS_WIN} 52 | REQUIRED) 53 | elseif(OS_MACOS) 54 | find_package( 55 | Qt${FIND_QT_VERSION} 56 | COMPONENTS ${FIND_QT_COMPONENTS} ${FIND_QT_COMPONENTS_MAC} 57 | REQUIRED) 58 | else() 59 | find_package( 60 | Qt${FIND_QT_VERSION} 61 | COMPONENTS ${FIND_QT_COMPONENTS} ${FIND_QT_COMPONENTS_LINUX} 62 | REQUIRED) 63 | endif() 64 | 65 | if("Gui" IN_LIST FIND_QT_COMPONENTS) 66 | list(APPEND FIND_QT_COMPONENTS "GuiPrivate") 67 | endif() 68 | 69 | foreach(_COMPONENT IN LISTS FIND_QT_COMPONENTS FIND_QT_COMPONENTS_WIN 70 | FIND_QT_COMPONENTS_MAC FIND_QT_COMPONENTS_LINUX) 71 | if(NOT TARGET Qt::${_COMPONENT} AND TARGET 72 | Qt${FIND_QT_VERSION}::${_COMPONENT}) 73 | 74 | add_library(Qt::${_COMPONENT} INTERFACE IMPORTED) 75 | set_target_properties( 76 | Qt::${_COMPONENT} PROPERTIES INTERFACE_LINK_LIBRARIES 77 | "Qt${FIND_QT_VERSION}::${_COMPONENT}") 78 | endif() 79 | endforeach() 80 | endmacro() 81 | 82 | file(RELATIVE_PATH RELATIVE_INSTALL_PATH ${CMAKE_SOURCE_DIR} ${CMAKE_INSTALL_PREFIX}) 83 | file(RELATIVE_PATH RELATIVE_BUILD_PATH ${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR}) 84 | 85 | if(OS_MACOS) 86 | set(CMAKE_OSX_ARCHITECTURES "x86_64" CACHE STRING "OBS build architecture for macOS - x86_64 required at least") 87 | set_property(CACHE CMAKE_OSX_ARCHITECTURES PROPERTY STRINGS x86_64 arm64 "x86_64;arm64") 88 | 89 | set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "OBS deployment target for macOS - 10.13+ required") 90 | set_property(CACHE CMAKE_OSX_DEPLOYMENT_TARGET PROPERTY STRINGS 10.15 11 12) 91 | 92 | set(OBS_BUNDLE_CODESIGN_IDENTITY "-" CACHE STRING "OBS code signing identity for macOS") 93 | set(OBS_CODESIGN_LINKER ON 94 | CACHE BOOL "Enable linker code-signing on macOS (macOS 11+ required)") 95 | 96 | if(XCODE) 97 | # Tell Xcode to pretend the linker signed binaries so that editing with 98 | # install_name_tool preserves ad-hoc signatures. This option is supported by 99 | # codesign on macOS 11 or higher. See CMake Issue 21854: 100 | # https://gitlab.kitware.com/cmake/cmake/-/issues/21854 101 | 102 | set(CMAKE_XCODE_GENERATE_SCHEME ON) 103 | endif() 104 | 105 | # Set default options for bundling on macOS 106 | set(CMAKE_MACOSX_RPATH ON) 107 | set(CMAKE_SKIP_BUILD_RPATH OFF) 108 | set(CMAKE_BUILD_WITH_INSTALL_RPATH OFF) 109 | set(CMAKE_INSTALL_RPATH "@executable_path/../Frameworks/") 110 | set(CMAKE_INSTALL_RPATH_USE_LINK_PATH OFF) 111 | 112 | function(setup_plugin_target target) 113 | if(NOT DEFINED MACOSX_PLUGIN_GUI_IDENTIFIER) 114 | message( 115 | FATAL_ERROR 116 | "No 'MACOSX_PLUGIN_GUI_IDENTIFIER' set, but is required to build plugin bundles on macOS - example: 'com.yourname.pluginname'" 117 | ) 118 | endif() 119 | 120 | if(NOT DEFINED MACOSX_PLUGIN_BUNDLE_VERSION) 121 | message( 122 | FATAL_ERROR 123 | "No 'MACOSX_PLUGIN_BUNDLE_VERSION' set, but is required to build plugin bundles on macOS - example: '25'" 124 | ) 125 | endif() 126 | 127 | if(NOT DEFINED MACOSX_PLUGIN_SHORT_VERSION_STRING) 128 | message( 129 | FATAL_ERROR 130 | "No 'MACOSX_PLUGIN_SHORT_VERSION_STRING' set, but is required to build plugin bundles on macOS - example: '1.0.2'" 131 | ) 132 | endif() 133 | 134 | set(MACOSX_PLUGIN_BUNDLE_NAME "${target}" PARENT_SCOPE) 135 | set(MACOSX_PLUGIN_BUNDLE_VERSION "${MACOSX_BUNDLE_BUNDLE_VERSION}" PARENT_SCOPE) 136 | set(MACOSX_PLUGIN_SHORT_VERSION_STRING "${MACOSX_BUNDLE_SHORT_VERSION_STRING}" PARENT_SCOPE) 137 | set(MACOSX_PLUGIN_EXECUTABLE_NAME "${target}" PARENT_SCOPE) 138 | 139 | if("${MACOSX_PLUGIN_BUNDLE_TYPE}" STREQUAL "BNDL") 140 | message(STATUS "Bundle type plugin") 141 | 142 | install( 143 | TARGETS ${target} 144 | LIBRARY DESTINATION "." 145 | COMPONENT obs_plugins 146 | NAMELINK_COMPONENT ${target}_Development) 147 | 148 | set_target_properties( 149 | ${target} 150 | PROPERTIES 151 | BUNDLE ON 152 | BUNDLE_EXTENSION "plugin" 153 | OUTPUT_NAME ${target} 154 | MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/bundle/macOS/Plugin-Info.plist.in" 155 | XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${MACOSX_PLUGIN_GUI_IDENTIFIER}" 156 | XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "${OBS_BUNDLE_CODESIGN_IDENTITY}" 157 | XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/bundle/macOS/entitlements.plist") 158 | 159 | install_bundle_resources(${target}) 160 | 161 | set(FIRST_DIR_SUFFIX ".plugin" PARENT_SCOPE) 162 | else() 163 | message(STATUS "Old type plugin") 164 | 165 | install( 166 | TARGETS ${target} 167 | LIBRARY DESTINATION "${target}/bin/" 168 | COMPONENT obs_plugins 169 | NAMELINK_COMPONENT ${target}_Development) 170 | 171 | if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/data) 172 | install( 173 | DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data/ 174 | DESTINATION "${target}/data/" 175 | USE_SOURCE_PERMISSIONS 176 | COMPONENT obs_plugins) 177 | endif() 178 | set(FIRST_DIR_SUFFIX "" PARENT_SCOPE) 179 | endif() 180 | 181 | endfunction() 182 | 183 | function(install_bundle_resources target) 184 | if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/data) 185 | file(GLOB_RECURSE _DATA_FILES "${CMAKE_CURRENT_SOURCE_DIR}/data/*") 186 | foreach(_DATA_FILE IN LISTS _DATA_FILES) 187 | file(RELATIVE_PATH _RELATIVE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/data/ 188 | ${_DATA_FILE}) 189 | get_filename_component(_RELATIVE_PATH ${_RELATIVE_PATH} PATH) 190 | target_sources(${target} PRIVATE ${_DATA_FILE}) 191 | set_source_files_properties( 192 | ${_DATA_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION 193 | Resources/${_RELATIVE_PATH}) 194 | string(REPLACE "\\" "\\\\" _GROUP_NAME "${_RELATIVE_PATH}") 195 | source_group("Resources\\${_GROUP_NAME}" FILES ${_DATA_FILE}) 196 | endforeach() 197 | endif() 198 | endfunction() 199 | 200 | else() 201 | if(CMAKE_SIZEOF_VOID_P EQUAL 8) 202 | set(_ARCH_SUFFIX 64) 203 | else() 204 | set(_ARCH_SUFFIX 32) 205 | endif() 206 | set(OBS_OUTPUT_DIR ${CMAKE_BINARY_DIR}/rundir) 207 | 208 | if(OS_POSIX) 209 | option(LINUX_PORTABLE "Build portable version (Linux)" ON) 210 | option(LINUX_RPATH "Set runpath (Linux)" ON) 211 | if(NOT LINUX_PORTABLE) 212 | set(OBS_LIBRARY_DESTINATION ${CMAKE_INSTALL_LIBDIR}) 213 | set(OBS_PLUGIN_DESTINATION ${OBS_LIBRARY_DESTINATION}/obs-plugins) 214 | if (LINUX_RPATH) 215 | set(CMAKE_INSTALL_RPATH ${CMAKE_INSTALL_PREFIX}/lib) 216 | endif() 217 | set(OBS_DATA_DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/obs) 218 | else() 219 | set(OBS_LIBRARY_DESTINATION bin/${_ARCH_SUFFIX}bit) 220 | set(OBS_PLUGIN_DESTINATION obs-plugins/${_ARCH_SUFFIX}bit) 221 | if (LINUX_RPATH) 222 | set(CMAKE_INSTALL_RPATH "$ORIGIN/" "${CMAKE_INSTALL_PREFIX}/${OBS_LIBRARY_DESTINATION}") 223 | endif() 224 | set(OBS_DATA_DESTINATION "data") 225 | endif() 226 | 227 | if(OS_LINUX) 228 | set(CPACK_PACKAGE_NAME "${PROJECT_NAME}") 229 | set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${LINUX_MAINTAINER_EMAIL}") 230 | set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") 231 | set(PKG_SUFFIX "-linux-x86_64" CACHE STRING "Suffix of package name") 232 | set(CPACK_PACKAGE_FILE_NAME 233 | "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}${PKG_SUFFIX}") 234 | 235 | set(CPACK_GENERATOR "DEB") 236 | 237 | if(NOT LINUX_PORTABLE) 238 | set(CPACK_SET_DESTDIR ON) 239 | endif() 240 | include(CPack) 241 | endif() 242 | else() 243 | set(OBS_LIBRARY_DESTINATION "bin/${_ARCH_SUFFIX}bit") 244 | set(OBS_LIBRARY32_DESTINATION "bin/32bit") 245 | set(OBS_LIBRARY64_DESTINATION "bin/64bit") 246 | set(OBS_PLUGIN_DESTINATION "obs-plugins/${_ARCH_SUFFIX}bit") 247 | set(OBS_PLUGIN32_DESTINATION "obs-plugins/32bit") 248 | set(OBS_PLUGIN64_DESTINATION "obs-plugins/64bit") 249 | 250 | set(OBS_DATA_DESTINATION "data") 251 | endif() 252 | 253 | function(setup_plugin_target target) 254 | set_target_properties(${target} PROPERTIES PREFIX "") 255 | 256 | install( 257 | TARGETS ${target} 258 | RUNTIME DESTINATION "${OBS_PLUGIN_DESTINATION}" 259 | COMPONENT ${target}_Runtime 260 | LIBRARY DESTINATION "${OBS_PLUGIN_DESTINATION}" 261 | COMPONENT ${target}_Runtime 262 | NAMELINK_COMPONENT ${target}_Development) 263 | 264 | install( 265 | FILES $ 266 | DESTINATION $/${OBS_PLUGIN_DESTINATION} 267 | COMPONENT obs_rundir 268 | EXCLUDE_FROM_ALL) 269 | 270 | if(OS_WINDOWS) 271 | install( 272 | FILES $ 273 | CONFIGURATIONS "RelWithDebInfo" "Debug" 274 | DESTINATION ${OBS_PLUGIN_DESTINATION} 275 | COMPONENT ${target}_Runtime 276 | OPTIONAL) 277 | 278 | install( 279 | FILES $ 280 | CONFIGURATIONS "RelWithDebInfo" "Debug" 281 | DESTINATION $/${OBS_PLUGIN_DESTINATION} 282 | COMPONENT obs_rundir 283 | OPTIONAL EXCLUDE_FROM_ALL) 284 | endif() 285 | 286 | if(MSVC) 287 | target_link_options( 288 | ${target} 289 | PRIVATE 290 | "LINKER:/OPT:REF" 291 | "$<$>:LINKER\:/SAFESEH\:NO>" 292 | "$<$:LINKER\:/INCREMENTAL:NO>" 293 | "$<$:LINKER\:/INCREMENTAL:NO>") 294 | endif() 295 | 296 | setup_target_resources(${target} obs-plugins/${target}) 297 | 298 | if(OS_WINDOWS) 299 | add_custom_command( 300 | TARGET ${target} 301 | POST_BUILD 302 | COMMAND 303 | "${CMAKE_COMMAND}" -DCMAKE_INSTALL_PREFIX=${OBS_OUTPUT_DIR} 304 | -DCMAKE_INSTALL_COMPONENT=obs_rundir 305 | -DCMAKE_INSTALL_CONFIG_NAME=$ -P 306 | ${CMAKE_CURRENT_BINARY_DIR}/cmake_install.cmake 307 | COMMENT "Installing to plugin rundir" 308 | VERBATIM) 309 | endif() 310 | endfunction() 311 | 312 | function(setup_target_resources target destination) 313 | if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/data) 314 | install( 315 | DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data/ 316 | DESTINATION ${OBS_DATA_DESTINATION}/${destination} 317 | USE_SOURCE_PERMISSIONS 318 | COMPONENT obs_plugins) 319 | 320 | install( 321 | DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data 322 | DESTINATION $/${OBS_DATA_DESTINATION}/${destination} 323 | USE_SOURCE_PERMISSIONS 324 | COMPONENT obs_rundir 325 | EXCLUDE_FROM_ALL) 326 | endif() 327 | endfunction() 328 | endif() 329 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Plugin Build 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | branches: 8 | - main 9 | tags: 10 | - '*' 11 | pull_request: 12 | paths-ignore: 13 | - '**.md' 14 | branches: 15 | - main 16 | 17 | env: 18 | artifactName: ${{ contains(github.ref_name, '/') && 'artifact' || github.ref_name }} 19 | qt: false 20 | 21 | jobs: 22 | linux_build: 23 | runs-on: ${{ matrix.ubuntu }} 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | obs: [27, 28] 28 | ubuntu: ['ubuntu-22.04'] 29 | defaults: 30 | run: 31 | shell: bash 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | with: 36 | submodules: recursive 37 | 38 | - name: Download obs-studio development environment 39 | id: obsdeps 40 | uses: norihiro/obs-studio-devel-action@v1-beta 41 | with: 42 | obs: ${{ matrix.obs }} 43 | verbose: true 44 | qt: ${{ env.qt }} 45 | 46 | - name: Build plugin 47 | run: | 48 | set -ex 49 | sudo apt install libpango1.0-dev libpng-dev 50 | OBS_QT_VERSION_MAJOR=${{ steps.obsdeps.outputs.OBS_QT_VERSION_MAJOR }} 51 | mkdir build 52 | cd build 53 | case ${{ matrix.obs }} in 54 | 27) 55 | cmake_opt=( 56 | -D CMAKE_INSTALL_LIBDIR=/usr/lib/ 57 | -D CPACK_DEBIAN_PACKAGE_DEPENDS='obs-studio (>= 27), obs-studio (<< 28)' 58 | ) 59 | ;; 60 | 28) 61 | cmake_opt=( 62 | -D CPACK_DEBIAN_PACKAGE_DEPENDS='obs-studio (>= 28)' 63 | ) 64 | ;; 65 | esac 66 | cmake .. \ 67 | -D QT_VERSION=$OBS_QT_VERSION_MAJOR \ 68 | -D CMAKE_INSTALL_PREFIX=/usr \ 69 | -D CMAKE_BUILD_TYPE=RelWithDebInfo \ 70 | -D LINUX_PORTABLE=OFF \ 71 | -D CPACK_DEBIAN_PACKAGE_SHLIBDEPS=ON \ 72 | -D PKG_SUFFIX=-obs${{ matrix.obs }}-${{ matrix.ubuntu }}-x86_64 \ 73 | "${cmake_opt[@]}" 74 | make -j4 75 | make package 76 | echo "FILE_NAME=$(find $PWD -name '*.deb' | head -n 1)" >> $GITHUB_ENV 77 | - name: Upload build artifact 78 | uses: actions/upload-artifact@v4 79 | with: 80 | name: ${{ env.artifactName }}-linux-obs${{ matrix.obs }}-${{ matrix.ubuntu }} 81 | path: '${{ env.FILE_NAME }}' 82 | - name: Check package 83 | run: | 84 | . build/ci/ci_includes.generated.sh 85 | set -ex 86 | sudo apt install '${{ env.FILE_NAME }}' 87 | case ${{ matrix.obs }} in 88 | 27) plugins_dir=/usr/lib/obs-plugins ;; 89 | 28) plugins_dir=/usr/lib/x86_64-linux-gnu/obs-plugins ;; 90 | esac 91 | ldd $plugins_dir/${PLUGIN_NAME}.so > ldd.out 92 | if grep not.found ldd.out ; then 93 | echo "Error: unresolved shared object." >&2 94 | exit 1 95 | fi 96 | ls /usr/share/obs/obs-plugins/${PLUGIN_NAME}/ 97 | 98 | macos_build: 99 | runs-on: macos-13 100 | strategy: 101 | fail-fast: false 102 | matrix: 103 | include: 104 | - obs: 27 105 | arch: x86_64 106 | - obs: 28 107 | arch: x86_64 108 | - obs: 28 109 | arch: arm64 110 | defaults: 111 | run: 112 | shell: bash 113 | steps: 114 | - name: Checkout 115 | uses: actions/checkout@v4 116 | with: 117 | submodules: recursive 118 | 119 | - name: Setup Environment 120 | id: setup 121 | run: | 122 | set -e 123 | echo '::group::Set up code signing' 124 | if [[ '${{ secrets.MACOS_SIGNING_APPLICATION_IDENTITY }}' != '' && \ 125 | '${{ secrets.MACOS_SIGNING_INSTALLER_IDENTITY }}' != '' && \ 126 | '${{ secrets.MACOS_SIGNING_CERT }}' != '' ]]; then 127 | echo "haveCodesignIdent=true" >> $GITHUB_OUTPUT 128 | else 129 | echo "haveCodesignIdent=false" >> $GITHUB_OUTPUT 130 | fi 131 | if [[ '${{ secrets.MACOS_NOTARIZATION_USERNAME }}' != '' && \ 132 | '${{ secrets.MACOS_NOTARIZATION_PASSWORD }}' != '' ]]; then 133 | echo "haveNotarizationUser=true" >> $GITHUB_OUTPUT 134 | else 135 | echo "haveNotarizationUser=false" >> $GITHUB_OUTPUT 136 | fi 137 | echo '::endgroup::' 138 | 139 | - name: Install Apple Developer Certificate 140 | if: ${{ github.event_name != 'pull_request' && steps.setup.outputs.haveCodesignIdent == 'true' }} 141 | uses: apple-actions/import-codesign-certs@253ddeeac23f2bdad1646faac5c8c2832e800071 142 | with: 143 | keychain-password: ${{ github.run_id }} 144 | p12-file-base64: ${{ secrets.MACOS_SIGNING_CERT }} 145 | p12-password: ${{ secrets.MACOS_SIGNING_CERT_PASSWORD }} 146 | 147 | - name: Set Signing Identity 148 | if: ${{ startsWith(github.ref, 'refs/tags/') && github.event_name != 'pull_request' && steps.setup.outputs.haveCodesignIdent == 'true' }} 149 | run: | 150 | set -e 151 | TEAM_ID=$(echo "${{ secrets.MACOS_SIGNING_APPLICATION_IDENTITY }}" | sed 's/.*(\([A-Za-z0-9]*\))$/\1/') 152 | xcrun notarytool store-credentials AC_PASSWORD \ 153 | --apple-id "${{ secrets.MACOS_NOTARIZATION_USERNAME }}" \ 154 | --team-id "$TEAM_ID" \ 155 | --password "${{ secrets.MACOS_NOTARIZATION_PASSWORD }}" 156 | 157 | - name: Download obs-studio development environment 158 | id: obsdeps 159 | uses: norihiro/obs-studio-devel-action@v1-beta 160 | with: 161 | path: /tmp/deps-${{ matrix.obs }}-${{ matrix.arch }} 162 | arch: ${{ matrix.arch }} 163 | obs: ${{ matrix.obs }} 164 | verbose: true 165 | qt: ${{ env.qt }} 166 | 167 | - name: Install pango 168 | run: | 169 | export arch=${{ matrix.arch }} 170 | export deps=/tmp/deps-${{ matrix.obs }}-${{ matrix.arch }} 171 | ci/macos/install-pango-${{ matrix.arch }}.sh 172 | 173 | - name: Build plugin 174 | run: | 175 | arch=${{ matrix.arch }} 176 | deps=/tmp/deps-${{ matrix.obs }}-${{ matrix.arch }} 177 | MACOSX_DEPLOYMENT_TARGET=${{ steps.obsdeps.outputs.MACOSX_DEPLOYMENT_TARGET }} 178 | OBS_QT_VERSION_MAJOR=${{ steps.obsdeps.outputs.OBS_QT_VERSION_MAJOR }} 179 | GIT_TAG=$(git describe --tags --always) 180 | PKG_SUFFIX=-${GIT_TAG}-obs${{ matrix.obs }}-macos-${{ matrix.arch }} 181 | if test "$arch" = 'arm64'; then 182 | export PKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig:$PKG_CONFIG_PATH" 183 | fi 184 | set -e 185 | case "${{ matrix.obs }}" in 186 | 27) 187 | cmake_opt=() 188 | ;; 189 | 28) 190 | cmake_opt=( 191 | -D MACOSX_PLUGIN_BUNDLE_TYPE=BNDL 192 | ) 193 | ;; 194 | esac 195 | cmake -S . -B build -G Ninja \ 196 | -D QT_VERSION=$OBS_QT_VERSION_MAJOR \ 197 | -DCMAKE_BUILD_TYPE=RelWithDebInfo \ 198 | -DCMAKE_PREFIX_PATH="$PWD/release/" \ 199 | -DCMAKE_OSX_ARCHITECTURES=$arch \ 200 | -DCMAKE_OSX_DEPLOYMENT_TARGET=${MACOSX_DEPLOYMENT_TARGET} \ 201 | -DCMAKE_FRAMEWORK_PATH="$deps/Frameworks;$deps/lib/cmake;$deps" \ 202 | -D PKG_SUFFIX=$PKG_SUFFIX \ 203 | "${cmake_opt[@]}" 204 | cmake --build build --config RelWithDebInfo 205 | 206 | - name: Prepare package 207 | run: | 208 | set -ex 209 | . build/ci/ci_includes.generated.sh 210 | cmake --install build --config RelWithDebInfo --prefix=release 211 | case ${{ matrix.obs }} in 212 | 27) 213 | (cd release/${PLUGIN_NAME} && ../../ci/macos/change-rpath.sh -obs ${{ matrix.obs }} -lib lib/ bin/${PLUGIN_NAME}.so) 214 | cp LICENSE release/${PLUGIN_NAME}/data/LICENSE-$PLUGIN_NAME 215 | ci/macos/test-dylib.sh -27 release/${PLUGIN_NAME}/bin/${PLUGIN_NAME}.so 216 | ;; 217 | 28) 218 | (cd release/${PLUGIN_NAME}.plugin/Contents && ../../../ci/macos/change-rpath.sh -obs 28 -lib lib/ MacOS/${PLUGIN_NAME}) 219 | cp LICENSE release/${PLUGIN_NAME}.plugin/Contents/Resources/LICENSE-$PLUGIN_NAME 220 | ci/macos/test-dylib.sh -28 release/${PLUGIN_NAME}.plugin/Contents/MacOS/${PLUGIN_NAME} 221 | ;; 222 | esac 223 | 224 | - name: Codesign 225 | if: ${{ github.event_name != 'pull_request' && steps.setup.outputs.haveCodesignIdent == 'true' }} 226 | run: | 227 | . build/ci/ci_includes.generated.sh 228 | set -e 229 | case ${{ matrix.obs }} in 230 | 27) 231 | files=( 232 | $(find release/${PLUGIN_NAME}/ -name '*.dylib') 233 | release/${PLUGIN_NAME}/bin/${PLUGIN_NAME}.so 234 | ) 235 | ;; 236 | 28) 237 | files=( 238 | $(find release/${PLUGIN_NAME}.plugin/ -name '*.dylib') 239 | release/${PLUGIN_NAME}.plugin/Contents/MacOS/${PLUGIN_NAME} 240 | ) 241 | ;; 242 | esac 243 | for dylib in "${files[@]}"; do 244 | codesign --force --sign "${{ secrets.MACOS_SIGNING_APPLICATION_IDENTITY }}" "$dylib" 245 | done 246 | for dylib in "${files[@]}"; do 247 | codesign -vvv --deep --strict "$dylib" 248 | done 249 | 250 | - name: Package 251 | run: | 252 | . build/ci/ci_includes.generated.sh 253 | set -ex 254 | zipfile=$PWD/package/${PLUGIN_NAME}${PKG_SUFFIX}.zip 255 | mkdir package 256 | case ${{ matrix.obs }} in 257 | 27) (cd release/ && zip -r $zipfile ${PLUGIN_NAME}) ;; 258 | 28) (cd release/ && zip -r $zipfile ${PLUGIN_NAME}.plugin) ;; 259 | esac 260 | ci/macos/install-packagesbuild.sh 261 | packagesbuild \ 262 | --build-folder $PWD/package/ \ 263 | build/installer-macOS.generated.pkgproj 264 | 265 | - name: Productsign 266 | if: ${{ github.event_name != 'pull_request' && steps.setup.outputs.haveCodesignIdent == 'true' }} 267 | run: | 268 | . build/ci/ci_includes.generated.sh 269 | pkgfile=package/${PLUGIN_NAME}${PKG_SUFFIX}.pkg 270 | set -e 271 | . build/ci/ci_includes.generated.sh 272 | productsign --sign "${{ secrets.MACOS_SIGNING_INSTALLER_IDENTITY }}" $pkgfile package/${PLUGIN_NAME}-signed.pkg 273 | mv package/${PLUGIN_NAME}-signed.pkg $pkgfile 274 | 275 | - name: Notarize 276 | if: ${{ startsWith(github.ref, 'refs/tags/') && github.event_name != 'pull_request' && steps.setup.outputs.haveCodesignIdent == 'true' }} 277 | uses: norihiro/macos-notarize-action@v1 278 | with: 279 | path: package/* 280 | keychainProfile: AC_PASSWORD 281 | verbose: true 282 | 283 | - name: Upload build artifact 284 | uses: actions/upload-artifact@v4 285 | with: 286 | name: ${{ env.artifactName }}-macos-obs${{ matrix.obs }}-${{ matrix.arch }} 287 | path: package/* 288 | 289 | windows_build: 290 | runs-on: windows-2022 291 | strategy: 292 | fail-fast: false 293 | matrix: 294 | obs: [31] 295 | arch: [x64] 296 | env: 297 | visualStudio: 'Visual Studio 17 2022' 298 | Configuration: 'RelWithDebInfo' 299 | defaults: 300 | run: 301 | shell: pwsh 302 | steps: 303 | - name: Checkout 304 | uses: actions/checkout@v4 305 | with: 306 | submodules: recursive 307 | - name: Download obs-studio 308 | id: obsdeps 309 | uses: norihiro/obs-studio-devel-action@v2 310 | with: 311 | obs: ${{ matrix.obs }} 312 | qt: ${{ env.qt }} 313 | 314 | - uses: actions/cache/restore@v4 315 | with: 316 | path: ${{ github.workspace }}/.vcpkg-binary-cache 317 | key: vcpkg-binary-cache-${{ runner.os }}-${{ matrix.arch }}-${{ hashFiles('.github/workflows/main.yml') }} 318 | restore-keys: | 319 | vcpkg-binary-cache-${{ runner.os }}-${{ matrix.arch }}- 320 | 321 | - name: Setup dependencies 322 | run: | 323 | $ErrorActionPreference = "Stop" 324 | mkdir -force ${{ github.workspace }}\.vcpkg-binary-cache > $null 325 | $env:VCPKG_BINARY_SOURCES="files,${{ github.workspace }}\.vcpkg-binary-cache,readwrite" 326 | git clone https://github.com/microsoft/vcpkg.git vcpkg 327 | .\vcpkg\bootstrap-vcpkg.bat 328 | .\vcpkg\vcpkg.exe install pango cairo --triplet x64-windows-static 329 | if ($LASTEXITCODE) { throw } 330 | .\vcpkg\vcpkg.exe install pkgconf:x64-windows 331 | if ($LASTEXITCODE) { throw } 332 | 333 | - uses: actions/cache/save@v4 334 | with: 335 | path: ${{ github.workspace }}/.vcpkg-binary-cache 336 | key: vcpkg-binary-cache-${{ runner.os }}-${{ matrix.arch }}-${{ hashFiles('.github/workflows/main.yml') }} 337 | 338 | - name: Remove obs- prefix 339 | shell: bash 340 | run: | 341 | sed -i -e 's;project(obs-;project(;' CMakeLists.txt 342 | 343 | - name: Build plugin 344 | run: | 345 | $ErrorActionPreference = "Stop" 346 | $CmakeArgs = @( 347 | '-G', "${{ env.visualStudio }}" 348 | '-DCMAKE_SYSTEM_VERSION=10.0.18363.657' 349 | '-DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake' 350 | '-DVCPKG_TARGET_TRIPLET=x64-windows-static' 351 | '-DPKG_CONFIG_EXECUTABLE=${{ github.workspace }}/vcpkg/installed/x64-windows/tools/pkgconf/pkgconf.exe' 352 | '-DCMAKE_BUILD_TYPE=RelWithDebInfo' 353 | ) 354 | cmake -S . -B build ${{ steps.obsdeps.outputs.PLUGIN_CMAKE_OPTIONS_PS }} @CmakeArgs 355 | cmake --build build --config RelWithDebInfo -j 4 356 | cmake --install build --config RelWithDebInfo --prefix "$(Resolve-Path -Path .)/release" 357 | 358 | - name: Package plugin 359 | run: ci/windows/package-windows.cmd ${{ matrix.obs }} 360 | 361 | - name: Upload build artifact 362 | uses: actions/upload-artifact@v4 363 | with: 364 | name: ${{ env.artifactName }}-windows-obs${{ matrix.obs }}-${{ matrix.arch }} 365 | path: package/* 366 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License 307 | along with this program; if not, write to the Free Software 308 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 309 | 310 | 311 | Also add information on how to contact you by electronic and paper mail. 312 | 313 | If the program is interactive, make it output a short notice like this 314 | when it starts in an interactive mode: 315 | 316 | Gnomovision version 69, Copyright (C) year name of author 317 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 318 | This is free software, and you are welcome to redistribute it 319 | under certain conditions; type `show c' for details. 320 | 321 | The hypothetical commands `show w' and `show c' should show the appropriate 322 | parts of the General Public License. Of course, the commands you use may 323 | be called something other than `show w' and `show c'; they could even be 324 | mouse-clicks or menu items--whatever suits your program. 325 | 326 | You should also get your employer (if you work as a programmer) or your 327 | school, if any, to sign a "copyright disclaimer" for the program, if 328 | necessary. Here is a sample; alter the names: 329 | 330 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 331 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 332 | 333 | , 1 April 1989 334 | Ty Coon, President of Vice 335 | 336 | This General Public License does not permit incorporating your program into 337 | proprietary programs. If your program is a subroutine library, you may 338 | consider it more useful to permit linking proprietary applications with the 339 | library. If this is what you want to do, use the GNU Library General 340 | Public License instead of this License. 341 | -------------------------------------------------------------------------------- /src/obs-text-pthread-thread.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "plugin-macros.generated.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #ifndef _WIN32 8 | #include 9 | #endif 10 | #include 11 | #include 12 | #ifdef PNG_FOUND 13 | #include 14 | #endif // PNG_FOUND 15 | #include 16 | #include "obs-text-pthread.h" 17 | 18 | #define debug(format, ...) 19 | // #define debug(format, ...) fprintf(stderr, format, ##__VA_ARGS__) 20 | 21 | #define GAUSSIAN_RANGE 2 22 | 23 | static char *tp_load_text_file(struct tp_config *config) 24 | { 25 | if (!config->text_file) 26 | return NULL; 27 | 28 | FILE *fp = fopen(config->text_file, "rb"); 29 | if (!fp) 30 | return NULL; 31 | 32 | fseek(fp, 0, SEEK_END); 33 | size_t len = ftell(fp); 34 | char *buf = bmalloc(len + 1); 35 | buf[len] = 0; 36 | 37 | fseek(fp, 0, SEEK_SET); 38 | size_t ret = fread(buf, 1, len, fp); 39 | if (ret != len) 40 | blog(LOG_ERROR, "Error reading a file '%s', fread returns %d", config->text_file, (int)ret); 41 | 42 | fclose(fp); 43 | 44 | debug("tp_load_text returns \"%s\"\n", buf); 45 | return buf; 46 | } 47 | 48 | static double u32toFR(uint32_t u) 49 | { 50 | return (double)((u >> 0) & 0xFF) / 255.; 51 | } 52 | static double u32toFG(uint32_t u) 53 | { 54 | return (double)((u >> 8) & 0xFF) / 255.; 55 | } 56 | static double u32toFB(uint32_t u) 57 | { 58 | return (double)((u >> 16) & 0xFF) / 255.; 59 | } 60 | static double u32toFA(uint32_t u) 61 | { 62 | return (double)((u >> 24) & 0xFF) / 255.; 63 | } 64 | 65 | static inline int blur_step(int blur) 66 | { 67 | // only odd number is allowed 68 | // roughly 16 steps to draw with pango-cairo, then blur by pixel. 69 | return (blur / 8) | 1; 70 | } 71 | 72 | static void tp_stroke_path(cairo_t *cr, PangoLayout *layout, const struct tp_config *config, int offset_x, int offset_y, 73 | uint32_t color, int width, int blur) 74 | { 75 | bool path_preserved = false; 76 | bool blur_gaussian = config->outline_blur_gaussian; 77 | const int bs = blur_step(blur); 78 | int b_end = blur_gaussian ? -blur * GAUSSIAN_RANGE : -blur; 79 | if (blur && b_end + width <= 0) 80 | b_end = -width + 1; 81 | int b_start = blur_gaussian ? blur * GAUSSIAN_RANGE : blur; 82 | if (bs > 1) 83 | b_start = b_end + (b_start - b_end + bs - 1) / bs * bs; 84 | double a_prev = 0.0; 85 | for (int b = b_start; b >= b_end; b -= bs) { 86 | double a; 87 | if (!blur) 88 | a = 1.0; 89 | else if (blur_gaussian) { 90 | int bs1 = bs ? bs + 1 : 0; 91 | a = 0.5 - erff((float)(b - bs1 * 0.5f) / blur) * 0.5; 92 | } 93 | else 94 | a = 0.5 - b * 0.5 / blur; 95 | a *= u32toFA(color); 96 | 97 | // skip this loop if quantized alpha code is same as that in the previous. 98 | if (blur && (int)(a * 255 + 0.5) == (int)(a_prev * 255 + 0.5)) 99 | continue; 100 | a_prev = a; 101 | 102 | int w = (width + b) * 2; 103 | if (w < 0) 104 | break; 105 | 106 | cairo_move_to(cr, offset_x, offset_y); 107 | cairo_set_source_rgba(cr, u32toFR(color), u32toFG(color), u32toFB(color), a); 108 | if (w > 0) { 109 | cairo_set_line_width(cr, w); 110 | if (config->outline_shape & OUTLINE_BEVEL) { 111 | cairo_set_line_join(cr, CAIRO_LINE_JOIN_BEVEL); 112 | } 113 | else if (config->outline_shape & OUTLINE_RECT) { 114 | cairo_set_line_join(cr, CAIRO_LINE_JOIN_MITER); 115 | cairo_set_miter_limit(cr, 1.999); 116 | } 117 | else if (config->outline_shape & OUTLINE_SHARP) { 118 | cairo_set_line_join(cr, CAIRO_LINE_JOIN_MITER); 119 | cairo_set_miter_limit(cr, 3.999); 120 | } 121 | else { 122 | cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND); 123 | } 124 | if (!path_preserved) 125 | pango_cairo_layout_path(cr, layout); 126 | cairo_stroke_preserve(cr); 127 | path_preserved = true; 128 | } 129 | else { 130 | pango_cairo_show_layout(cr, layout); 131 | } 132 | } 133 | 134 | cairo_surface_flush(cairo_get_target(cr)); 135 | 136 | if (bs > 1) { 137 | cairo_surface_t *surface = cairo_get_target(cr); 138 | const int w = cairo_image_surface_get_width(surface); 139 | const int h = cairo_image_surface_get_height(surface); 140 | if (w <= 0 || h <= 0) 141 | return; 142 | uint8_t *data = cairo_image_surface_get_data(surface); 143 | const int bs1 = bs + 1; 144 | uint32_t *tmp = bzalloc(sizeof(uint32_t) * w * bs1); 145 | uint32_t **tt = bzalloc(sizeof(uint32_t *) * bs1); 146 | 147 | for (int k = 0; k < bs1; k++) { 148 | tt[k] = tmp + w * k; 149 | } 150 | 151 | const int bs2 = bs / 2; 152 | const int div = bs * bs; 153 | for (int k = 0, kt = 0; k < h; k++) { 154 | const int k2 = k + bs2 < h ? k + bs2 : h - 1; 155 | const int k1 = k - bs2 - 1; 156 | 157 | for (; kt <= k2; kt++) { 158 | for (int i = 0; i < w; i++) { 159 | uint32_t s = data[(i + kt * w) * 4 + 3]; 160 | if (i > 0) 161 | s += tt[kt % bs1][i - 1]; 162 | if (kt > 0) 163 | s += +tt[(kt - 1) % bs1][i]; 164 | if (kt > 0 && i > 0) 165 | s -= tt[(kt - 1) % bs1][i - 1]; 166 | tt[kt % bs1][i] = s; 167 | } 168 | } 169 | 170 | for (int i = 0; i < w; i++) { 171 | const int i2 = i + bs2 < w ? i + bs2 : w - 1; 172 | const int i1 = i - bs2 - 1; 173 | uint32_t s = tt[k2 % bs1][i2]; 174 | if (k1 >= 0) 175 | s -= tt[k1 % bs1][i2]; 176 | if (i1 >= 0) 177 | s -= tt[k2 % bs1][i1]; 178 | if (k1 >= 0 && i1 >= 0) 179 | s += tt[k1 % bs1][i1]; 180 | s /= div; 181 | if (s > 255) 182 | s = 255; 183 | data[(i + k * w) * 4 + 3] = s; 184 | } 185 | } 186 | 187 | bfree(tmp); 188 | bfree(tt); 189 | } 190 | } 191 | 192 | static inline uint32_t blend_text_ch(uint32_t xat, uint32_t xb, uint32_t at, uint32_t ab, uint32_t u) 193 | { 194 | // u: factor for the bottom color 195 | return xat + xb * ab * u * (255 - at) / (255 * 255 * 255); 196 | } 197 | 198 | static inline uint32_t blend_text(uint32_t cat, uint32_t cb, uint32_t u) 199 | { 200 | uint32_t a_255 = (cat >> 24) * 255 + u * (cb >> 24) - (cat >> 24) * u * (cb >> 24) / 255; 201 | if (a_255 < 255) 202 | return 0; // completely transparent 203 | return ((a_255 / 255) << 24) | 204 | (blend_text_ch((cat >> 16) & 0xFF, (cb >> 16) & 0xFF, cat >> 24, cb >> 24, u) << 16) | 205 | (blend_text_ch((cat >> 8) & 0xFF, (cb >> 8) & 0xFF, cat >> 24, cb >> 24, u) << 8) | 206 | (blend_text_ch(cat & 0xFF, cb & 0xFF, cat >> 24, cb >> 24, u)); 207 | } 208 | 209 | static inline void blend_shadow(uint8_t *s, const int stride, const uint32_t h, const uint8_t *ss, uint32_t cs) 210 | { 211 | uint32_t size = h * stride; 212 | for (uint32_t i = 0, k = 0; i < size; i += 4, k += 1) 213 | if (ss[k]) { 214 | uint32_t ct = s ? s[i] << 16 | s[i + 1] << 8 | s[i + 2] | s[i + 3] << 24 : 0; 215 | uint32_t c = blend_text(ct, cs, ss[k]); 216 | s[i] = c >> 16; 217 | s[i + 1] = c >> 8; 218 | s[i + 2] = c; 219 | s[i + 3] = c >> 24; 220 | } 221 | } 222 | 223 | static struct tp_texture *tp_draw_texture(struct tp_config *config, char *text) 224 | { 225 | struct tp_texture *n = bzalloc(sizeof(struct tp_texture)); 226 | 227 | int outline_width = config->outline ? config->outline_width : 0; 228 | int outline_blur = config->outline ? config->outline_blur : 0; 229 | bool outline_blur_gaussian = config->outline_blur_gaussian; 230 | int outline_width_blur = outline_width + (outline_blur_gaussian ? outline_blur * GAUSSIAN_RANGE : outline_blur); 231 | if (config->outline_shape & OUTLINE_SHARP) 232 | outline_width_blur *= 2; 233 | int shadow_abs_x = config->shadow ? abs(config->shadow_x) : 0; 234 | int shadow_abs_y = config->shadow ? abs(config->shadow_y) : 0; 235 | int offset_x = outline_width_blur + (config->shadow && config->shadow_x < 0 ? -config->shadow_x : 0); 236 | int offset_y = outline_width_blur + (config->shadow && config->shadow_y < 0 ? -config->shadow_y : 0); 237 | 238 | uint32_t body_width = config->width; 239 | uint32_t surface_width = body_width + outline_width_blur * 2 + shadow_abs_x; 240 | uint32_t surface_height = config->height + outline_width_blur * 2 + shadow_abs_y; 241 | 242 | int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, surface_width); 243 | 244 | if (!surface_width || !surface_height || stride <= 0) { 245 | blog(LOG_ERROR, "nothing to draw: %ux%u stride=%d", surface_width, surface_height, stride); 246 | return n; 247 | } 248 | 249 | n->surface = bzalloc(stride * surface_height); 250 | 251 | cairo_surface_t *surface = cairo_image_surface_create_for_data(n->surface, CAIRO_FORMAT_ARGB32, surface_width, 252 | surface_height, stride); 253 | 254 | cairo_t *cr = cairo_create(surface); 255 | 256 | PangoLayout *layout = pango_cairo_create_layout(cr); 257 | 258 | debug("font name=<%s> style=<%s> size=%d flags=0x%X\n", config->font_name, config->font_style, 259 | config->font_size, config->font_flags); 260 | PangoFontDescription *desc = pango_font_description_new(); 261 | pango_font_description_set_family(desc, config->font_name); 262 | pango_font_description_set_weight(desc, (config->font_flags & OBS_FONT_BOLD) ? PANGO_WEIGHT_BOLD : 0); 263 | pango_font_description_set_style(desc, (config->font_flags & OBS_FONT_ITALIC) ? PANGO_STYLE_ITALIC : 0); 264 | pango_font_description_set_size(desc, (config->font_size * PANGO_SCALE * 2) / 265 | 3); // Scaling to approximate GDI text pts 266 | pango_layout_set_font_description(layout, desc); 267 | pango_font_description_free(desc); 268 | 269 | if (config->align & ALIGN_CENTER) 270 | pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER); 271 | else if (config->align & ALIGN_RIGHT) 272 | pango_layout_set_alignment(layout, PANGO_ALIGN_RIGHT); 273 | else // ALIGN_LEFT 274 | pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT); 275 | pango_layout_set_justify(layout, !!(config->align & ALIGN_JUSTIFY)); 276 | pango_layout_set_indent(layout, config->indent * PANGO_SCALE); 277 | 278 | pango_layout_set_width(layout, body_width << 10); 279 | pango_layout_set_auto_dir(layout, config->auto_dir); 280 | pango_layout_set_wrap(layout, config->wrapmode); 281 | pango_layout_set_ellipsize(layout, config->ellipsize); 282 | pango_layout_set_spacing(layout, config->spacing * PANGO_SCALE); 283 | 284 | (config->markup ? pango_layout_set_markup : pango_layout_set_text)(layout, text, -1); 285 | 286 | PangoRectangle ink_rect, logical_rect; 287 | pango_layout_get_extents(layout, &ink_rect, &logical_rect); 288 | uint32_t surface_ink_height = PANGO_PIXELS_FLOOR(ink_rect.height) + PANGO_PIXELS_FLOOR(ink_rect.y) + 289 | outline_width_blur * 2 + shadow_abs_y; 290 | uint32_t surface_ink_height1 = surface_height > surface_ink_height ? surface_ink_height : surface_height; 291 | 292 | if (outline_width_blur > 0) { 293 | debug("stroking outline width=%d\n", outline_width); 294 | cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); 295 | tp_stroke_path(cr, layout, config, offset_x, offset_y, config->outline_color, outline_width, 296 | outline_blur); 297 | 298 | // overwrite outline color 299 | uint32_t size = stride * surface_height; 300 | uint8_t *ptr = n->surface; 301 | uint8_t c[4] = {config->outline_color >> 16, config->outline_color >> 8, config->outline_color, 0}; 302 | for (uint32_t i = 0; i < size; i += 4) { 303 | int a = ptr[3]; 304 | for (int k = 0; k < 3; k++) { 305 | int x = a * c[k] / 255; 306 | if (x > 255) 307 | x = 255; 308 | ptr[k] = x; 309 | } 310 | ptr += 4; 311 | } 312 | } 313 | 314 | cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); 315 | tp_stroke_path(cr, layout, config, offset_x, offset_y, config->color, 0, 0); 316 | 317 | if (shadow_abs_x || shadow_abs_y) { 318 | uint8_t *surface_shadow = bzalloc(stride * surface_height); 319 | uint8_t *dst = surface_shadow; 320 | uint8_t *src = n->surface; 321 | if (config->shadow_x > 0) 322 | dst += shadow_abs_x; 323 | else 324 | src += shadow_abs_x * 4; 325 | if (config->shadow_y > 0) 326 | dst += shadow_abs_y * stride / 4; 327 | else 328 | src += shadow_abs_y * stride; 329 | 330 | for (int y = 0; y < (int)surface_ink_height1 - shadow_abs_y; y++) { 331 | uint8_t *d = dst; 332 | dst += stride / 4; 333 | uint8_t *s = src + 3; 334 | src += stride; 335 | for (int x = 0; x < (int)surface_width - shadow_abs_x; x++) { 336 | *d = *s; 337 | d += 1; 338 | s += 4; 339 | } 340 | } 341 | 342 | blend_shadow(n->surface, stride, surface_ink_height1, surface_shadow, config->shadow_color); 343 | bfree(surface_shadow); 344 | } 345 | 346 | g_object_unref(layout); 347 | cairo_destroy(cr); 348 | cairo_surface_destroy(surface); 349 | 350 | if (config->shrink_size) { 351 | int xoff = PANGO_PIXELS_FLOOR(logical_rect.x); 352 | if (xoff < 0) { 353 | n->width = PANGO_PIXELS_CEIL(logical_rect.x + logical_rect.width) + outline_width_blur * 2 + 354 | shadow_abs_x; 355 | xoff = 0; 356 | } 357 | else 358 | n->width = PANGO_PIXELS_CEIL(logical_rect.width) + outline_width_blur * 2 + shadow_abs_x; 359 | if (n->width > surface_width) 360 | n->width = surface_width; 361 | n->height = 362 | PANGO_PIXELS_CEIL(logical_rect.y + logical_rect.height) + outline_width_blur * 2 + shadow_abs_y; 363 | if (n->height > surface_height) 364 | n->height = surface_height; 365 | if (n->width != surface_width) { 366 | uint32_t new_stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, n->width); 367 | for (uint32_t y = 1; y < n->height; y++) { 368 | memmove(n->surface + new_stride * y, n->surface + stride * y + xoff * 4, new_stride); 369 | } 370 | } 371 | } 372 | else { 373 | n->width = surface_width; 374 | n->height = surface_height; 375 | } 376 | 377 | debug("tp_draw_texture end: width=%d height=%d \n", n->width, n->height); 378 | 379 | return n; 380 | } 381 | 382 | static bool tp_compare_stat(const struct stat *a, const struct stat *b) 383 | { 384 | if (a->st_ino != b->st_ino) 385 | return true; 386 | if (a->st_size != b->st_size) 387 | return true; 388 | #ifdef __USE_XOPEN2K8 389 | if (memcmp(&a->st_mtim, &b->st_mtim, sizeof(struct timespec))) 390 | return true; 391 | #else // __USE_XOPEN2K8 392 | if (a->st_mtime != b->st_mtime) 393 | return true; 394 | #ifdef _STATBUF_ST_NSEC 395 | if (a->st_mtimensec != b->st_mtimensec) 396 | return true; 397 | #endif // _STATBUF_ST_NSEC 398 | #endif // __USE_XOPEN2K8 399 | return false; 400 | } 401 | 402 | static inline bool is_printable(const char *t) 403 | { 404 | for (; *t; t++) { 405 | const char c = *t; 406 | if (!(c == ' ' || c == '\n' || c == '\t' || c == '\r')) 407 | return true; 408 | } 409 | return false; 410 | } 411 | 412 | #ifdef PNG_FOUND 413 | static void png_list_write_config(FILE *fp, const struct tp_config *config, const struct tp_config *prev) 414 | { 415 | #define WRITE_IF_UPDATED(val, fmt) \ 416 | if (!prev || config->val != prev->val) \ 417 | fprintf(fp, "#\t" #val ":\t%" fmt "\n", config->val) 418 | WRITE_IF_UPDATED(fadein_ms, PRIu32); 419 | WRITE_IF_UPDATED(fadeout_ms, PRIu32); 420 | WRITE_IF_UPDATED(crossfade_ms, PRIu32); 421 | } 422 | 423 | static FILE *fopen_png_list(uint64_t ns, const struct tp_config *config) 424 | { 425 | uint64_t ms = ns / 1000000; 426 | size_t n = strlen(config->save_file_dir) + 24; 427 | char *fname = bmalloc(n); 428 | snprintf(fname, n, "%s/list-%08ds%03d.dat", config->save_file_dir, (int)(ms / 1000), (int)(ms % 1000)); 429 | FILE *fp = fopen(fname, "w"); 430 | bfree(fname); 431 | if (fp) 432 | png_list_write_config(fp, config, NULL); 433 | return fp; 434 | } 435 | 436 | static void save_to_png(const uint8_t *surface, int width, int height, uint64_t ns, FILE *fp_png_list, 437 | const struct tp_config *config) 438 | { 439 | uint64_t ms = ns / 1000000; 440 | size_t n = strlen(config->save_file_dir) + 24; 441 | char *fname = bmalloc(n); 442 | snprintf(fname, n, "%s/text-%08ds%03d.png", config->save_file_dir, (int)(ms / 1000), (int)(ms % 1000)); 443 | FILE *fp = fopen(fname, "wb"); 444 | if (!fp) { 445 | blog(LOG_ERROR, "text-pthread: save_to_png: failed to open %s", fname); 446 | bfree(fname); 447 | return; 448 | } 449 | 450 | png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); 451 | if (!png_ptr) { 452 | blog(LOG_ERROR, "text-pthread: save_to_png: png_create_write_struct failed"); 453 | fclose(fp); 454 | bfree(fname); 455 | return; 456 | } 457 | // TODO: use png_create_write_struct_2 instead so that bzalloc and bfree can check memory leak 458 | 459 | png_infop info_ptr = png_create_info_struct(png_ptr); 460 | if (!info_ptr) { 461 | blog(LOG_ERROR, "text-pthread: save_to_png: png_create_info_struct failed"); 462 | png_destroy_write_struct(&png_ptr, NULL); 463 | fclose(fp); 464 | bfree(fname); 465 | return; 466 | } 467 | 468 | if (setjmp(png_jmpbuf(png_ptr))) { 469 | blog(LOG_ERROR, "text-pthread: save_to_png: png_jmpbuf failed"); 470 | png_destroy_write_struct(&png_ptr, &info_ptr); 471 | fclose(fp); 472 | bfree(fname); 473 | return; 474 | } 475 | 476 | png_init_io(png_ptr, fp); 477 | 478 | // you may call png_set_filter to tune the speed 479 | 480 | png_set_IHDR(png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE, 481 | PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); 482 | png_write_info(png_ptr, info_ptr); 483 | for (int y = 0; y < height; y++) { 484 | png_write_row(png_ptr, surface + y * width * 4); 485 | } 486 | 487 | png_write_end(png_ptr, info_ptr); 488 | png_destroy_write_struct(&png_ptr, &info_ptr); 489 | fclose(fp); 490 | 491 | if (fp_png_list) { 492 | fprintf(fp_png_list, "%" PRIu64 "\ttext-%08ds%03d.png\t%d\t%d\n", ms, (int)(ms / 1000), 493 | (int)(ms % 1000), width, height); 494 | } 495 | bfree(fname); 496 | } 497 | 498 | static void png_list_empty(uint64_t ns, FILE *fp_png_list) 499 | { 500 | uint64_t ms = ns / 1000000; 501 | fprintf(fp_png_list, "%" PRIu64 "\t-\n", ms); 502 | } 503 | 504 | static inline int cnvpm2npm(int c, int a) 505 | { 506 | if (a == 0 || a == 255) 507 | return c; 508 | c = c * 255 / a; 509 | if (c > 255) 510 | return 255; 511 | return c; 512 | } 513 | #endif // PNG_FOUND 514 | 515 | static void *tp_thread_main(void *data) 516 | { 517 | struct tp_source *src = data; 518 | 519 | struct stat st_prev = {0}; 520 | struct tp_config config_prev = {0}; 521 | bool b_printable_prev = false; 522 | #ifdef PNG_FOUND 523 | FILE *fp_png_list = NULL; 524 | #endif // PNG_FOUND 525 | 526 | #ifndef _WIN32 527 | setpriority(PRIO_PROCESS, 0, 19); 528 | #endif 529 | os_set_thread_name("text-pthread"); 530 | 531 | while (os_atomic_load_bool(&src->running)) { 532 | os_sleep_ms(33); 533 | 534 | pthread_mutex_lock(&src->config_mutex); 535 | 536 | bool config_updated = src->config_updated; 537 | bool text_updated = false; 538 | #ifdef PNG_FOUND 539 | bool save_file_updated = false; 540 | #endif // PNG_FOUND 541 | 542 | // check config and copy 543 | if (config_updated) { 544 | if (!config_prev.from_file && !src->config.from_file && config_prev.text && src->config.text && 545 | strcmp(config_prev.text, src->config.text)) 546 | text_updated = true; 547 | #ifdef PNG_FOUND 548 | if (config_prev.save_file != src->config.save_file) 549 | save_file_updated = true; 550 | else if (!src->config.save_file) 551 | save_file_updated = false; 552 | else if (strcmp(config_prev.save_file_dir, src->config.save_file_dir)) 553 | save_file_updated = true; 554 | if (fp_png_list && !save_file_updated) 555 | png_list_write_config(fp_png_list, &src->config, &config_prev); 556 | #endif // PNG_FOUND 557 | 558 | BFREE_IF_NONNULL(config_prev.font_name); 559 | BFREE_IF_NONNULL(config_prev.font_style); 560 | BFREE_IF_NONNULL(config_prev.text); 561 | BFREE_IF_NONNULL(config_prev.text_file); 562 | #ifdef PNG_FOUND 563 | BFREE_IF_NONNULL(config_prev.save_file_dir); 564 | #endif // PNG_FOUND 565 | memcpy(&config_prev, &src->config, sizeof(struct tp_config)); 566 | config_prev.font_name = bstrdup(src->config.font_name); 567 | config_prev.font_style = bstrdup(src->config.font_style); 568 | if (!config_prev.from_file) { 569 | config_prev.text = bstrdup(src->config.text); 570 | config_prev.text_file = NULL; 571 | } 572 | else { 573 | config_prev.text = NULL; 574 | config_prev.text_file = bstrdup(src->config.text_file); 575 | } 576 | #ifdef PNG_FOUND 577 | config_prev.save_file_dir = src->config.save_file ? bstrdup(src->config.save_file_dir) : NULL; 578 | #endif // PNG_FOUND 579 | src->config_updated = 0; 580 | } 581 | 582 | pthread_mutex_unlock(&src->config_mutex); 583 | 584 | // check file status 585 | if (config_prev.from_file) { 586 | struct stat st = {0}; 587 | os_stat(config_prev.text_file, &st); 588 | if (tp_compare_stat(&st, &st_prev)) { 589 | text_updated = 1; 590 | memcpy(&st_prev, &st, sizeof(struct stat)); 591 | } 592 | } 593 | 594 | // TODO: how long will it take to draw a new texture? 595 | // If it takes much longer than frame rate, it should notify the main thread to start fade-out. 596 | 597 | // load file if changed and draw 598 | if (config_updated || text_updated) { 599 | uint64_t time_ns = os_gettime_ns(); 600 | char *text = config_prev.from_file ? tp_load_text_file(&config_prev) : config_prev.text; 601 | bool b_printable = text ? is_printable(text) : 0; 602 | #ifdef PNG_FOUND 603 | uint8_t *png_surface = NULL; 604 | int png_width = 0; 605 | int png_height = 0; 606 | #endif // PNG_FOUND 607 | 608 | // make an early notification 609 | if (b_printable) { 610 | os_atomic_set_bool(&src->text_updating, true); 611 | } 612 | 613 | struct tp_texture *tex; 614 | if (b_printable) { 615 | tex = tp_draw_texture(&config_prev, text); 616 | #ifdef PNG_FOUND 617 | if (config_prev.save_file && tex->width > 0 && tex->height > 0) { 618 | png_width = tex->width; 619 | png_height = tex->height; 620 | png_surface = bzalloc(4 * png_width * png_height); 621 | if (png_surface) 622 | for (int i = 0, size = png_width * png_height; i < size; i++) { 623 | int a = tex->surface[i * 4 + 3]; 624 | png_surface[i * 4 + 0] = cnvpm2npm(tex->surface[i * 4 + 2], a); 625 | png_surface[i * 4 + 1] = cnvpm2npm(tex->surface[i * 4 + 1], a); 626 | png_surface[i * 4 + 2] = cnvpm2npm(tex->surface[i * 4 + 0], a); 627 | png_surface[i * 4 + 3] = a; 628 | } 629 | } 630 | #endif // PNG_FOUND 631 | } 632 | else { 633 | tex = bzalloc(sizeof(struct tp_texture)); 634 | } 635 | tex->time_ns = time_ns; 636 | tex->config_updated = config_updated && !text_updated; 637 | tex->is_crossfade = b_printable && b_printable_prev && text_updated; 638 | 639 | pthread_mutex_lock(&src->tex_mutex); 640 | src->tex_new = pushback_texture(src->tex_new, tex); 641 | tex = NULL; 642 | pthread_mutex_unlock(&src->tex_mutex); 643 | 644 | if (config_prev.from_file) 645 | BFREE_IF_NONNULL(text); 646 | 647 | debug("tp_draw_texture & tp_draw_texture takes %f ms\n", (os_gettime_ns() - time_ns) * 1e-6); 648 | 649 | if (text_updated) 650 | b_printable_prev = b_printable; 651 | 652 | #ifdef PNG_FOUND 653 | if (save_file_updated) { 654 | if (fp_png_list) { 655 | fclose(fp_png_list); 656 | fp_png_list = NULL; 657 | } 658 | if (config_prev.save_file) { 659 | fp_png_list = fopen_png_list(time_ns, &config_prev); 660 | } 661 | } 662 | if (png_surface && fp_png_list) { 663 | save_to_png(png_surface, png_width, png_height, time_ns, fp_png_list, &config_prev); 664 | } 665 | else if (fp_png_list) { 666 | png_list_empty(time_ns, fp_png_list); 667 | } 668 | BFREE_IF_NONNULL(png_surface); 669 | #endif // PNG_FOUND 670 | } 671 | } 672 | 673 | #ifdef PNG_FOUND 674 | if (fp_png_list) { 675 | png_list_empty(os_gettime_ns(), fp_png_list); 676 | fclose(fp_png_list); 677 | } 678 | #endif // PNG_FOUND 679 | tp_config_destroy_member(&config_prev); 680 | return NULL; 681 | } 682 | 683 | void tp_thread_start(struct tp_source *src) 684 | { 685 | os_atomic_set_bool(&src->running, true); 686 | pthread_create(&src->thread, NULL, tp_thread_main, src); 687 | } 688 | 689 | void tp_thread_end(struct tp_source *src) 690 | { 691 | os_atomic_set_bool(&src->running, false); 692 | pthread_join(src->thread, NULL); 693 | } 694 | -------------------------------------------------------------------------------- /src/obs-text-pthread-main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "plugin-macros.generated.h" 6 | #include "obs-text-pthread.h" 7 | 8 | OBS_DECLARE_MODULE() 9 | OBS_MODULE_USE_DEFAULT_LOCALE(PLUGIN_NAME, "en-US") 10 | 11 | #define debug(format, ...) 12 | // #define debug(format, ...) fprintf(stderr, format, ##__VA_ARGS__) 13 | 14 | static inline uint64_t max_u64(uint64_t a, uint64_t b) 15 | { 16 | if (a > b) 17 | return a; 18 | return b; 19 | } 20 | 21 | static gs_effect_t *textalpha_effect = NULL; 22 | 23 | #define tp_data_get_color(s, c) tp_data_get_color2(s, c, c ".alpha") 24 | static inline uint32_t tp_data_get_color2(obs_data_t *settings, const char *color, const char *alpha) 25 | { 26 | return ((uint32_t)obs_data_get_int(settings, color) & 0xFFFFFF) | 27 | ((uint32_t)obs_data_get_int(settings, alpha) & 0xFF) << 24; 28 | } 29 | 30 | #define tp_data_add_color(props, c, t) \ 31 | { \ 32 | obs_properties_add_color(props, c, t); \ 33 | obs_properties_add_int_slider(props, c ".alpha", obs_module_text("Alpha"), 0, 255, 1); \ 34 | } 35 | 36 | static const char *tp_get_name(void *unused) 37 | { 38 | UNUSED_PARAMETER(unused); 39 | 40 | return obs_module_text("Text Pthread"); 41 | } 42 | 43 | static void tp_update(void *data, obs_data_t *settings); 44 | 45 | static void initialize_textalpha_effect() 46 | { 47 | const char *filename = "textalpha.effect"; 48 | char *f = obs_module_file(filename); 49 | if (!f) { 50 | blog(LOG_ERROR, "Cannot find module file '%s'", filename); 51 | return; 52 | } 53 | 54 | textalpha_effect = gs_effect_create_from_file(f, NULL); 55 | if (!textalpha_effect) 56 | blog(LOG_ERROR, "Cannot load '%s'", f); 57 | 58 | bfree(f); 59 | } 60 | 61 | static void *tp_create(obs_data_t *settings, obs_source_t *source) 62 | { 63 | UNUSED_PARAMETER(source); 64 | struct tp_source *src = bzalloc(sizeof(struct tp_source)); 65 | 66 | obs_enter_graphics(); 67 | if (!textalpha_effect) 68 | initialize_textalpha_effect(); 69 | obs_leave_graphics(); 70 | 71 | pthread_mutex_init(&src->config_mutex, NULL); 72 | pthread_mutex_init(&src->tex_mutex, NULL); 73 | 74 | tp_update(src, settings); 75 | 76 | tp_thread_start(src); 77 | 78 | return src; 79 | } 80 | 81 | static void tp_destroy(void *data) 82 | { 83 | struct tp_source *src = data; 84 | 85 | tp_thread_end(src); 86 | 87 | tp_config_destroy_member(&src->config); 88 | 89 | if (src->textures) 90 | free_texture(src->textures); 91 | if (src->tex_new) 92 | free_texture(src->tex_new); 93 | 94 | pthread_mutex_destroy(&src->tex_mutex); 95 | pthread_mutex_destroy(&src->config_mutex); 96 | 97 | bfree(src); 98 | } 99 | 100 | static void tp_update(void *data, obs_data_t *settings) 101 | { 102 | struct tp_source *src = data; 103 | 104 | pthread_mutex_lock(&src->config_mutex); 105 | 106 | obs_data_t *font_obj = obs_data_get_obj(settings, "font"); 107 | if (font_obj) { 108 | BFREE_IF_NONNULL(src->config.font_name); 109 | src->config.font_name = bstrdup(obs_data_get_string(font_obj, "face")); 110 | 111 | BFREE_IF_NONNULL(src->config.font_style); 112 | src->config.font_style = bstrdup(obs_data_get_string(font_obj, "style")); 113 | 114 | src->config.font_size = (uint32_t)obs_data_get_int(font_obj, "size"); 115 | src->config.font_flags = (uint32_t)obs_data_get_int(font_obj, "flags"); 116 | 117 | obs_data_release(font_obj); 118 | } 119 | 120 | src->config.from_file = obs_data_get_bool(settings, "from_file"); 121 | BFREE_IF_NONNULL(src->config.text); 122 | BFREE_IF_NONNULL(src->config.text_file); 123 | if (!src->config.from_file) { 124 | src->config.text = bstrdup(obs_data_get_string(settings, "text")); 125 | } 126 | else { 127 | src->config.text_file = bstrdup(obs_data_get_string(settings, "text_file")); 128 | } 129 | src->config.markup = obs_data_get_bool(settings, "markup"); 130 | 131 | src->config.color = tp_data_get_color(settings, "color"); 132 | 133 | src->config.width = (uint32_t)obs_data_get_int(settings, "width"); 134 | src->config.height = (uint32_t)obs_data_get_int(settings, "height"); 135 | src->config.shrink_size = obs_data_get_bool(settings, "shrink_size"); 136 | src->config.align = (uint32_t)obs_data_get_int(settings, "align"); 137 | src->config.auto_dir = obs_data_get_bool(settings, "auto_dir"); 138 | src->config.wrapmode = (int)obs_data_get_int(settings, "wrapmode"); 139 | src->config.indent = (int32_t)obs_data_get_int(settings, "indent"); 140 | src->config.ellipsize = (int)obs_data_get_int(settings, "ellipsize"); 141 | src->config.spacing = (int)obs_data_get_int(settings, "spacing"); 142 | 143 | src->config.outline = obs_data_get_bool(settings, "outline"); 144 | src->config.outline_color = tp_data_get_color(settings, "outline_color"); 145 | src->config.outline_width = (uint32_t)obs_data_get_int(settings, "outline_width"); 146 | src->config.outline_blur = (uint32_t)obs_data_get_int(settings, "outline_blur"); 147 | src->config.outline_blur_gaussian = obs_data_get_bool(settings, "outline_blur_gaussian"); 148 | src->config.outline_shape = (uint32_t)obs_data_get_int(settings, "outline_shape"); 149 | 150 | src->config.shadow = obs_data_get_bool(settings, "shadow"); 151 | src->config.shadow_color = tp_data_get_color(settings, "shadow_color"); 152 | src->config.shadow_x = (int32_t)obs_data_get_int(settings, "shadow_x"); 153 | src->config.shadow_y = (int32_t)obs_data_get_int(settings, "shadow_y"); 154 | 155 | src->config.align_transition = (uint32_t)obs_data_get_int(settings, "align_transition.v") | 156 | (uint32_t)obs_data_get_int(settings, "align_transition.h"); 157 | 158 | src->config.fadein_ms = (uint32_t)obs_data_get_int(settings, "fadein_ms"); 159 | src->config.fadeout_ms = (uint32_t)obs_data_get_int(settings, "fadeout_ms"); 160 | src->config.crossfade_ms = (uint32_t)obs_data_get_int(settings, "crossfade_ms"); 161 | 162 | src->config.slide_pxps = (uint32_t)obs_data_get_int(settings, "slide_pxps"); 163 | 164 | #ifdef PNG_FOUND 165 | src->config.save_file = obs_data_get_bool(settings, "save_file"); 166 | BFREE_IF_NONNULL(src->config.save_file_dir); 167 | if (src->config.save_file) { 168 | const char *s = obs_data_get_string(settings, "save_file_dir"); 169 | if (*s) 170 | src->config.save_file_dir = bstrdup(s); 171 | else { 172 | blog(LOG_ERROR, "save_file_dir is not specified"); 173 | src->config.save_file = false; 174 | } 175 | } 176 | #endif // PNG_FOUND 177 | 178 | src->config_updated = true; 179 | 180 | pthread_mutex_unlock(&src->config_mutex); 181 | } 182 | 183 | static void tp_get_defaults(obs_data_t *settings) 184 | { 185 | obs_data_t *font_obj = obs_data_create(); 186 | obs_data_set_default_int(font_obj, "size", 64); 187 | obs_data_set_default_obj(settings, "font", font_obj); 188 | obs_data_release(font_obj); 189 | 190 | obs_data_set_default_bool(settings, "markup", true); 191 | 192 | obs_data_set_default_int(settings, "color", 0xFFFFFFFF); 193 | obs_data_set_default_int(settings, "color.alpha", 0xFF); 194 | 195 | obs_data_set_default_int(settings, "width", 1920); 196 | obs_data_set_default_int(settings, "height", 1080); 197 | obs_data_set_default_bool(settings, "shrink_size", true); 198 | obs_data_set_default_bool(settings, "auto_dir", true); 199 | obs_data_set_default_int(settings, "wrapmode", PANGO_WRAP_WORD); 200 | obs_data_set_default_int(settings, "ellipsize", PANGO_ELLIPSIZE_NONE); 201 | obs_data_set_default_int(settings, "spacing", 0); 202 | 203 | obs_data_set_default_int(settings, "outline_color.alpha", 0xFF); 204 | 205 | obs_data_set_default_int(settings, "shadow_x", 2); 206 | obs_data_set_default_int(settings, "shadow_y", 3); 207 | obs_data_set_default_int(settings, "shadow_color.alpha", 0xFF); 208 | 209 | obs_data_set_default_int(settings, "align_transition.v", ALIGN_TOP); 210 | obs_data_set_default_int(settings, "align_transition.h", ALIGN_LEFT); 211 | } 212 | 213 | static void tp_get_defaults_v2(obs_data_t *settings) 214 | { 215 | tp_get_defaults(settings); 216 | 217 | obs_data_set_default_bool(settings, "outline_blur_gaussian", true); 218 | } 219 | 220 | #define tp_set_visible(props, name, en) \ 221 | { \ 222 | obs_property_t *prop = obs_properties_get(props, name); \ 223 | if (prop) \ 224 | obs_property_set_visible(prop, en); \ 225 | } 226 | 227 | static bool tp_prop_outline_changed(obs_properties_t *props, obs_property_t *property, obs_data_t *settings) 228 | { 229 | UNUSED_PARAMETER(property); 230 | 231 | bool en = settings ? obs_data_get_bool(settings, "outline") : false; 232 | tp_set_visible(props, "outline_color", en); 233 | tp_set_visible(props, "outline_color.alpha", en); 234 | tp_set_visible(props, "outline_width", en); 235 | tp_set_visible(props, "outline_blur", en); 236 | tp_set_visible(props, "outline_blur_gaussian", en); 237 | tp_set_visible(props, "outline_shape", en); 238 | 239 | return true; 240 | } 241 | 242 | static bool tp_prop_shadow_changed(obs_properties_t *props, obs_property_t *property, obs_data_t *settings) 243 | { 244 | UNUSED_PARAMETER(property); 245 | 246 | bool en = settings ? obs_data_get_bool(settings, "shadow") : false; 247 | tp_set_visible(props, "shadow_color", en); 248 | tp_set_visible(props, "shadow_color.alpha", en); 249 | tp_set_visible(props, "shadow_x", en); 250 | tp_set_visible(props, "shadow_y", en); 251 | 252 | return true; 253 | } 254 | 255 | static obs_properties_t *tp_get_properties(void *unused) 256 | { 257 | UNUSED_PARAMETER(unused); 258 | obs_properties_t *props; 259 | obs_property_t *prop; 260 | props = obs_properties_create(); 261 | 262 | obs_properties_add_font(props, "font", obs_module_text("Font")); 263 | 264 | obs_properties_add_text(props, "text", obs_module_text("Text"), OBS_TEXT_MULTILINE); 265 | obs_properties_add_bool(props, "from_file", obs_module_text("Read text from a file")); 266 | obs_properties_add_path(props, "text_file", obs_module_text("Text file"), OBS_PATH_FILE, NULL, NULL); 267 | obs_properties_add_bool(props, "markup", obs_module_text("Pango mark-up")); 268 | 269 | tp_data_add_color(props, "color", obs_module_text("Color")); 270 | 271 | obs_properties_add_int(props, "width", obs_module_text("Width"), 1, 16384, 1); 272 | obs_properties_add_int(props, "height", obs_module_text("Height"), 1, 16384, 1); 273 | obs_properties_add_bool(props, "shrink_size", obs_module_text("Automatically shrink size")); 274 | 275 | prop = obs_properties_add_list(props, "align", obs_module_text("Alignment"), OBS_COMBO_TYPE_LIST, 276 | OBS_COMBO_FORMAT_INT); 277 | obs_property_list_add_int(prop, obs_module_text("Alignment.Left"), ALIGN_LEFT); 278 | obs_property_list_add_int(prop, obs_module_text("Alignment.Center"), ALIGN_CENTER); 279 | obs_property_list_add_int(prop, obs_module_text("Alignment.Right"), ALIGN_RIGHT); 280 | obs_property_list_add_int(prop, obs_module_text("Alignment.Left.Justify"), ALIGN_LEFT | ALIGN_JUSTIFY); 281 | obs_property_list_add_int(prop, obs_module_text("Alignment.Center.Justify"), ALIGN_CENTER | ALIGN_JUSTIFY); 282 | obs_property_list_add_int(prop, obs_module_text("Alignment.Right.Justify"), ALIGN_RIGHT | ALIGN_JUSTIFY); 283 | 284 | obs_properties_add_bool(props, "auto_dir", obs_module_text("Calculate the bidirectonal base direction")); 285 | 286 | prop = obs_properties_add_list(props, "wrapmode", obs_module_text("Wrap text"), OBS_COMBO_TYPE_LIST, 287 | OBS_COMBO_FORMAT_INT); 288 | obs_property_list_add_int(prop, obs_module_text("Wrapmode.Word"), PANGO_WRAP_WORD); 289 | obs_property_list_add_int(prop, obs_module_text("Wrapmode.Char"), PANGO_WRAP_CHAR); 290 | obs_property_list_add_int(prop, obs_module_text("Wrapmode.WordChar"), PANGO_WRAP_WORD_CHAR); 291 | 292 | obs_properties_add_int(props, "indent", obs_module_text("Indent"), -32767, 32767, 1); 293 | 294 | prop = obs_properties_add_list(props, "ellipsize", obs_module_text("Ellipsize"), OBS_COMBO_TYPE_LIST, 295 | OBS_COMBO_FORMAT_INT); 296 | obs_property_list_add_int(prop, obs_module_text("Ellipsize.None"), PANGO_ELLIPSIZE_NONE); 297 | obs_property_list_add_int(prop, obs_module_text("Ellipsize.Start"), PANGO_ELLIPSIZE_START); 298 | obs_property_list_add_int(prop, obs_module_text("Ellipsize.Middle"), PANGO_ELLIPSIZE_MIDDLE); 299 | obs_property_list_add_int(prop, obs_module_text("Ellipsize.End"), PANGO_ELLIPSIZE_END); 300 | 301 | obs_properties_add_int(props, "spacing", obs_module_text("Line spacing"), -65536, +65536, 1); 302 | 303 | // TODO: vertical 304 | 305 | prop = obs_properties_add_bool(props, "outline", obs_module_text("Outline")); 306 | obs_property_set_modified_callback(prop, tp_prop_outline_changed); 307 | tp_data_add_color(props, "outline_color", obs_module_text("Outline color")); 308 | obs_properties_add_int(props, "outline_width", obs_module_text("Outline width"), 0, 65536, 1); 309 | obs_properties_add_int(props, "outline_blur", obs_module_text("Outline blur"), 0, 65536, 1); 310 | obs_properties_add_bool(props, "outline_blur_gaussian", obs_module_text("Outline blur with gaussian function")); 311 | prop = obs_properties_add_list(props, "outline_shape", obs_module_text("Outline shape"), OBS_COMBO_TYPE_LIST, 312 | OBS_COMBO_FORMAT_INT); 313 | obs_property_list_add_int(prop, obs_module_text("Outline.Round"), OUTLINE_ROUND); 314 | obs_property_list_add_int(prop, obs_module_text("Outline.Bevel"), OUTLINE_BEVEL); 315 | obs_property_list_add_int(prop, obs_module_text("Outline.Rectangle"), OUTLINE_RECT); 316 | obs_property_list_add_int(prop, obs_module_text("Outline.Sharp"), OUTLINE_SHARP); 317 | 318 | prop = obs_properties_add_bool(props, "shadow", obs_module_text("Shadow")); 319 | obs_property_set_modified_callback(prop, tp_prop_shadow_changed); 320 | tp_data_add_color(props, "shadow_color", obs_module_text("Shadow color")); 321 | obs_properties_add_int(props, "shadow_x", obs_module_text("Shadow offset x"), -65536, 65536, 1); 322 | obs_properties_add_int(props, "shadow_y", obs_module_text("Shadow offset y"), -65536, 65536, 1); 323 | 324 | prop = obs_properties_add_list(props, "align_transition.h", obs_module_text("Transition alignment"), 325 | OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); 326 | obs_property_list_add_int(prop, obs_module_text("Alignment.Left"), ALIGN_LEFT); 327 | obs_property_list_add_int(prop, obs_module_text("Alignment.Center"), ALIGN_CENTER); 328 | obs_property_list_add_int(prop, obs_module_text("Alignment.Right"), ALIGN_RIGHT); 329 | 330 | prop = obs_properties_add_list(props, "align_transition.v", obs_module_text("Transition alignment"), 331 | OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); 332 | obs_property_list_add_int(prop, obs_module_text("Alignment.Top"), ALIGN_TOP); 333 | obs_property_list_add_int(prop, obs_module_text("Alignment.Center"), ALIGN_VCENTER); 334 | obs_property_list_add_int(prop, obs_module_text("Alignment.Bottom"), ALIGN_BOTTOM); 335 | 336 | obs_properties_add_int(props, "fadein_ms", obs_module_text("Fadein time [ms]"), 0, 4294, 100); 337 | obs_properties_add_int(props, "fadeout_ms", obs_module_text("Fadeout time [ms]"), 0, 4294, 100); 338 | obs_properties_add_int(props, "crossfade_ms", obs_module_text("Crossfade time [ms]"), 0, 4294, 100); 339 | 340 | obs_properties_add_int(props, "slide_pxps", obs_module_text("Slide [px/s] (0 to disable)"), 0, 65500, 50); 341 | 342 | #ifdef PNG_FOUND 343 | obs_properties_add_bool(props, "save_file", obs_module_text("Save as PNG")); 344 | obs_properties_add_path(props, "save_file_dir", obs_module_text("Directory to save"), OBS_PATH_DIRECTORY, NULL, 345 | NULL); 346 | #endif // PNG_FOUND 347 | 348 | return props; 349 | } 350 | 351 | static uint32_t tp_get_width(void *data) 352 | { 353 | struct tp_source *src = data; 354 | 355 | uint32_t w = 0; 356 | struct tp_texture *t = src->textures; 357 | while (t) { 358 | if (w < t->width) 359 | w = t->width; 360 | t = t->next; 361 | } 362 | 363 | return w; 364 | } 365 | 366 | static uint32_t tp_get_height(void *data) 367 | { 368 | struct tp_source *src = data; 369 | 370 | uint32_t h = 0; 371 | struct tp_texture *t = src->textures; 372 | while (t) { 373 | if (src->config.slide_pxps) 374 | h += t->slide_h; 375 | else { 376 | if (h < t->height) 377 | h = t->height; 378 | } 379 | t = t->next; 380 | } 381 | 382 | return h; 383 | } 384 | 385 | static void tp_surface_to_texture(struct tp_texture *t) 386 | { 387 | if (t->surface && !t->tex) { 388 | const uint8_t *surface = t->surface; 389 | t->tex = gs_texture_create(t->width, t->height, GS_BGRA, 1, &surface, 0); 390 | } 391 | } 392 | 393 | static void tp_render(void *data, gs_effect_t *effect) 394 | { 395 | UNUSED_PARAMETER(effect); 396 | struct tp_source *src = data; 397 | if (!textalpha_effect) 398 | return; 399 | 400 | obs_enter_graphics(); 401 | gs_blend_state_push(); 402 | gs_blend_function(GS_BLEND_ONE, GS_BLEND_INVSRCALPHA); 403 | 404 | const int w = tp_get_width(data); 405 | const int h = tp_get_height(data); 406 | int xoff = 0, yoff = 0; 407 | 408 | for (struct tp_texture *t = src->textures; t; t = t->next) { 409 | if (!t->width || !t->height) 410 | continue; 411 | if (t->slide_u > 0 && t->slide_u > (int)t->height) 412 | continue; 413 | if (t->slide_u < 0 && (-t->slide_u) > (int)t->height) 414 | continue; 415 | tp_surface_to_texture(t); 416 | int y0 = t->slide_u > 0 ? t->slide_u : 0; 417 | int y1 = t->slide_u < 0 ? t->height + t->slide_u : t->height; 418 | xoff = 0; 419 | if ((src->config.align_transition & ALIGN_RIGHT) && (int)t->width < w) 420 | xoff += w - t->width; 421 | else if ((src->config.align_transition & ALIGN_CENTER) && (int)t->width < w) 422 | xoff += w / 2 - t->width / 2; 423 | if (!src->config.slide_pxps) { 424 | yoff = 0; 425 | if ((src->config.align_transition & ALIGN_BOTTOM) && (int)t->height != h) 426 | yoff += h - t->height; 427 | else if ((src->config.align_transition & ALIGN_VCENTER) && (int)t->height != h) 428 | yoff += h / 2 - t->height / 2; 429 | } 430 | if (xoff || yoff) { 431 | gs_matrix_push(); 432 | gs_matrix_translate3f((float)xoff, (float)yoff, 0); 433 | } 434 | gs_effect_set_texture(gs_effect_get_param_by_name(textalpha_effect, "image"), t->tex); 435 | gs_effect_set_float(gs_effect_get_param_by_name(textalpha_effect, "alpha"), t->fade_alpha / 255.f); 436 | while (gs_effect_loop(textalpha_effect, "Draw")) { 437 | gs_draw_sprite_subregion(t->tex, 0, 0, y0, t->width, y1); 438 | } 439 | if (xoff || yoff) 440 | gs_matrix_pop(); 441 | 442 | if (src->config.slide_pxps) { 443 | yoff += t->slide_h; 444 | } 445 | } 446 | gs_blend_state_pop(); 447 | obs_leave_graphics(); 448 | } 449 | 450 | static inline void tp_load_new_texture(struct tp_source *src, uint64_t lastframe_ns) 451 | { 452 | if (src->tex_new) { 453 | // new texture arrived 454 | 455 | struct tp_texture *tn = src->tex_new; 456 | src->tex_new = tn->next; 457 | tn->next = NULL; 458 | 459 | if (tn->config_updated) { 460 | // A texture with updated config is arrived. 461 | if (src->textures) { 462 | free_texture(src->textures); 463 | src->textures = NULL; 464 | } 465 | } 466 | 467 | if (tn->surface) { 468 | // if non-blank texture 469 | 470 | if (!tn->config_updated) { 471 | tn->fadein_start_ns = lastframe_ns; 472 | tn->fadein_end_ns = 473 | lastframe_ns + 474 | (tn->is_crossfade ? src->config.crossfade_ms : src->config.fadein_ms) * 1000000; 475 | } 476 | 477 | if (src->config.slide_pxps) { 478 | uint64_t slidein_ns = tn->height * 1000000000LL / src->config.slide_pxps; 479 | if (!src->textures) { 480 | tn->slidein_end_ns = lastframe_ns + slidein_ns; 481 | } 482 | else if (src->textures) { 483 | struct tp_texture *tl = src->textures; 484 | while (tl->next) 485 | tl = tl->next; 486 | tn->slidein_end_ns = 487 | (tl->slideout_start_ns ? tl->slideout_start_ns : lastframe_ns) + 488 | slidein_ns; 489 | debug("lastframe_ns=%f %p slidein_end_ns=%f\n", lastframe_ns * 1e-9, tn, 490 | tn->slidein_end_ns * 1e-9); 491 | } 492 | } 493 | 494 | if (src->config.crossfade_ms == 0 && src->config.slide_pxps == 0) { 495 | if (src->textures) { 496 | free_texture(src->textures); 497 | src->textures = NULL; 498 | } 499 | } 500 | } 501 | else { 502 | // if blank texture 503 | 504 | // mark fadeout for old textures 505 | for (struct tp_texture *t = src->textures; t; t = t->next) { 506 | if (!t->fadeout_start_ns) { 507 | t->fadeout_start_ns = tn->time_ns; 508 | t->fadeout_end_ns = tn->time_ns + src->config.fadeout_ms * 1000000; 509 | } 510 | } 511 | } 512 | 513 | src->textures = pushback_texture(src->textures, tn); 514 | } 515 | } 516 | 517 | static struct tp_texture *tp_pop_old_textures(struct tp_texture *t, uint64_t now_ns, const struct tp_source *src) 518 | { 519 | if (!t) 520 | return NULL; 521 | 522 | bool deprecated = false; 523 | if (t->fadeout_end_ns) 524 | deprecated = true; 525 | if (t->slideout_start_ns) 526 | deprecated = true; 527 | if (t->next && !src->config.slide_pxps) 528 | deprecated = true; 529 | 530 | bool transition_ongoing = false; 531 | if (t->fadeout_end_ns && now_ns < t->fadeout_end_ns) 532 | transition_ongoing = true; 533 | if (t->slidein_end_ns) // TODO: remove me 534 | transition_ongoing = true; 535 | if (t->slideout_start_ns && t->slide_u < (int)t->height) 536 | transition_ongoing = true; 537 | if (t->slideout_start_ns && t->next && t->slide_u < (int)t->next->height) 538 | transition_ongoing = true; 539 | 540 | if (deprecated && !transition_ongoing) { 541 | debug("tp_pop_old_textures: removing %p fadeout_end=%f slideout_start=%f slide_u=%d height=%d now=%f\n", 542 | t, t->fadeout_end_ns * 1e-9, t->slideout_start_ns * 1e-9, t->slide_u, t->height, now_ns * 1e-9); 543 | t = popfront_texture(t); 544 | return tp_pop_old_textures(t, now_ns, src); 545 | } 546 | if (src->config.slide_pxps) 547 | return t; 548 | t->next = tp_pop_old_textures(t->next, now_ns, src); 549 | return t; 550 | } 551 | 552 | static void tp_tick(void *data, float seconds) 553 | { 554 | struct tp_source *src = data; 555 | 556 | uint64_t now_ns = os_gettime_ns(); 557 | uint64_t lastframe_ns = now_ns - (uint64_t)(seconds * 1e9); 558 | 559 | src->textures = tp_pop_old_textures(src->textures, now_ns, src); 560 | 561 | if (os_atomic_load_bool(&src->text_updating)) { 562 | // early notification for the new non-blank texture from the thread 563 | 564 | // mark fadeout for old textures 565 | if (src->config.crossfade_ms > 0) 566 | for (struct tp_texture *t = src->textures; t; t = t->next) { 567 | if (!t->fadeout_start_ns) { 568 | t->fadeout_start_ns = lastframe_ns; 569 | t->fadeout_end_ns = lastframe_ns + src->config.crossfade_ms * 1000000; 570 | } 571 | } 572 | 573 | if (src->config.slide_pxps) { 574 | uint64_t slideout_end_last = lastframe_ns; 575 | for (struct tp_texture *t = src->textures; t; t = t->next) { 576 | if (!t->slideout_start_ns) { 577 | t->slideout_start_ns = max_u64(t->slidein_end_ns, slideout_end_last); 578 | debug("now_ns=%f %p slideout_start_ns=%f slidein_end_ns=%f slideout_end_last=%f \n", 579 | now_ns * 1e-9, t, t->slideout_start_ns * 1e-9, t->slidein_end_ns * 1e-9, 580 | slideout_end_last * 1e-9); 581 | } 582 | slideout_end_last = 583 | t->slideout_start_ns + t->height * 1000000000LL / src->config.slide_pxps; 584 | debug("now_ns=%f %p slideout_end_last=%f slidetime=%f\n", now_ns * 1e-9, t, 585 | slideout_end_last * 1e-9, t->height * 1e0 / src->config.slide_pxps); 586 | } 587 | } 588 | 589 | os_atomic_set_bool(&src->text_updating, false); 590 | } 591 | 592 | if (pthread_mutex_trylock(&src->tex_mutex) == 0) { 593 | tp_load_new_texture(src, lastframe_ns); 594 | pthread_mutex_unlock(&src->tex_mutex); 595 | } 596 | 597 | if (src->config.slide_pxps) 598 | for (struct tp_texture *t = src->textures; t; t = t->next) { 599 | if (t->slidein_end_ns) { 600 | if (now_ns >= t->slidein_end_ns) { 601 | t->slidein_end_ns = 0; 602 | t->slide_u = 0; 603 | } 604 | else 605 | t->slide_u = -(int)((t->slidein_end_ns - now_ns) * src->config.slide_pxps / 606 | 1000000000); 607 | } 608 | 609 | if (!t->slidein_end_ns && t->slideout_start_ns && now_ns > t->slideout_start_ns) { 610 | t->slide_u = 611 | (int)((now_ns - t->slideout_start_ns) * src->config.slide_pxps / 1000000000); 612 | } 613 | 614 | t->slide_h = t->height - abs(t->slide_u); 615 | if (t->slide_h < 0) 616 | t->slide_h = 0; 617 | } 618 | 619 | src->textures = tp_pop_old_textures(src->textures, now_ns, src); 620 | 621 | for (struct tp_texture *t = src->textures; t; t = t->next) { 622 | 623 | // cleanup fadein 624 | if (t->fadein_start_ns && now_ns >= t->fadein_end_ns) { 625 | t->fadein_start_ns = t->fadein_end_ns = 0; 626 | } 627 | 628 | int fade_alpha = 255; 629 | 630 | if (t->fadein_start_ns) { 631 | fade_alpha = 632 | (int)(255 * (now_ns - t->fadein_start_ns) / (t->fadein_end_ns - t->fadein_start_ns)); 633 | } 634 | 635 | if (t->fadeout_end_ns && now_ns >= t->fadeout_end_ns) { 636 | fade_alpha = 0; 637 | } 638 | else if (t->fadeout_start_ns) { 639 | fade_alpha = (int)(fade_alpha * (t->fadeout_end_ns - now_ns) / 640 | (t->fadeout_end_ns - t->fadeout_start_ns)); 641 | } 642 | 643 | if (fade_alpha < 0) 644 | fade_alpha = 0; 645 | else if (fade_alpha > 255) 646 | fade_alpha = 255; 647 | t->fade_alpha = fade_alpha; 648 | } 649 | } 650 | 651 | static struct obs_source_info tp_src_info = { 652 | .id = "obs_text_pthread_source", 653 | .type = OBS_SOURCE_TYPE_INPUT, 654 | .output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW | OBS_SOURCE_CAP_OBSOLETE, 655 | .get_name = tp_get_name, 656 | .create = tp_create, 657 | .destroy = tp_destroy, 658 | .update = tp_update, 659 | .get_defaults = tp_get_defaults, 660 | .get_properties = tp_get_properties, 661 | .get_width = tp_get_width, 662 | .get_height = tp_get_height, 663 | .video_render = tp_render, 664 | .video_tick = tp_tick, 665 | .icon_type = OBS_ICON_TYPE_TEXT, 666 | }; 667 | 668 | static GLogWriterOutput glog_writer(GLogLevelFlags log_level, const GLogField *fields, gsize n_fields, 669 | gpointer user_data) 670 | { 671 | (void)user_data; 672 | 673 | int level; 674 | switch (log_level) { 675 | case G_LOG_FLAG_FATAL: 676 | case G_LOG_LEVEL_ERROR: 677 | case G_LOG_LEVEL_CRITICAL: 678 | level = LOG_ERROR; 679 | break; 680 | case G_LOG_LEVEL_WARNING: 681 | level = LOG_WARNING; 682 | break; 683 | case G_LOG_LEVEL_MESSAGE: 684 | case G_LOG_LEVEL_INFO: 685 | level = LOG_INFO; 686 | break; 687 | case G_LOG_LEVEL_DEBUG: 688 | level = LOG_DEBUG; 689 | break; 690 | default: 691 | level = LOG_INFO; 692 | } 693 | 694 | gchar *out = g_log_writer_format_fields(log_level, fields, n_fields, false); 695 | for (gchar *p = out; *p; p++) { 696 | if (*p == '\n') 697 | *p = ' '; 698 | } 699 | blog(level, "%s", out); 700 | g_free(out); 701 | 702 | return G_LOG_WRITER_HANDLED; 703 | } 704 | 705 | bool obs_module_load(void) 706 | { 707 | obs_register_source(&tp_src_info); 708 | 709 | struct obs_source_info tp_src_info_v2 = tp_src_info; 710 | tp_src_info_v2.get_defaults = tp_get_defaults_v2; 711 | tp_src_info_v2.version = 2; 712 | tp_src_info_v2.output_flags &= ~OBS_SOURCE_CAP_OBSOLETE; 713 | obs_register_source(&tp_src_info_v2); 714 | 715 | blog(LOG_INFO, "plugin loaded (version %s)", PLUGIN_VERSION); 716 | 717 | g_log_set_writer_func(glog_writer, NULL, NULL); 718 | 719 | return true; 720 | } 721 | 722 | void obs_module_unload() 723 | { 724 | if (textalpha_effect) { 725 | gs_effect_destroy(textalpha_effect); 726 | textalpha_effect = NULL; 727 | } 728 | blog(LOG_INFO, "plugin unloaded"); 729 | } 730 | -------------------------------------------------------------------------------- /installer/installer-macOS.pkgproj.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PACKAGES 6 | 7 | 8 | MUST-CLOSE-APPLICATION-ITEMS 9 | 10 | MUST-CLOSE-APPLICATIONS 11 | 12 | PACKAGE_FILES 13 | 14 | DEFAULT_INSTALL_LOCATION 15 | / 16 | HIERARCHY 17 | 18 | CHILDREN 19 | 20 | 21 | CHILDREN 22 | 23 | GID 24 | 80 25 | PATH 26 | Applications 27 | PATH_TYPE 28 | 0 29 | PERMISSIONS 30 | 509 31 | TYPE 32 | 1 33 | UID 34 | 0 35 | 36 | 37 | CHILDREN 38 | 39 | 40 | CHILDREN 41 | 42 | 43 | CHILDREN 44 | 45 | 46 | CHILDREN 47 | 48 | 49 | BUNDLE_CAN_DOWNGRADE 50 | 51 | BUNDLE_POSTINSTALL_PATH 52 | 53 | PATH_TYPE 54 | 0 55 | 56 | BUNDLE_PREINSTALL_PATH 57 | 58 | PATH_TYPE 59 | 0 60 | 61 | CHILDREN 62 | 63 | GID 64 | 80 65 | PATH 66 | ../@RELATIVE_INSTALL_PATH@/@PROJECT_NAME@@FIRST_DIR_SUFFIX@ 67 | PATH_TYPE 68 | 1 69 | PERMISSIONS 70 | 493 71 | TYPE 72 | 3 73 | UID 74 | 0 75 | 76 | 77 | GID 78 | 80 79 | PATH 80 | plugins 81 | PATH_TYPE 82 | 2 83 | PERMISSIONS 84 | 509 85 | TYPE 86 | 2 87 | UID 88 | 0 89 | 90 | 91 | GID 92 | 80 93 | PATH 94 | obs-studio 95 | PATH_TYPE 96 | 2 97 | PERMISSIONS 98 | 509 99 | TYPE 100 | 2 101 | UID 102 | 0 103 | 104 | 105 | GID 106 | 80 107 | PATH 108 | Application Support 109 | PATH_TYPE 110 | 0 111 | PERMISSIONS 112 | 493 113 | TYPE 114 | 1 115 | UID 116 | 0 117 | 118 | 119 | CHILDREN 120 | 121 | GID 122 | 0 123 | PATH 124 | Automator 125 | PATH_TYPE 126 | 0 127 | PERMISSIONS 128 | 493 129 | TYPE 130 | 1 131 | UID 132 | 0 133 | 134 | 135 | CHILDREN 136 | 137 | GID 138 | 0 139 | PATH 140 | Documentation 141 | PATH_TYPE 142 | 0 143 | PERMISSIONS 144 | 493 145 | TYPE 146 | 1 147 | UID 148 | 0 149 | 150 | 151 | CHILDREN 152 | 153 | GID 154 | 0 155 | PATH 156 | Extensions 157 | PATH_TYPE 158 | 0 159 | PERMISSIONS 160 | 493 161 | TYPE 162 | 1 163 | UID 164 | 0 165 | 166 | 167 | CHILDREN 168 | 169 | GID 170 | 0 171 | PATH 172 | Filesystems 173 | PATH_TYPE 174 | 0 175 | PERMISSIONS 176 | 493 177 | TYPE 178 | 1 179 | UID 180 | 0 181 | 182 | 183 | CHILDREN 184 | 185 | GID 186 | 0 187 | PATH 188 | Frameworks 189 | PATH_TYPE 190 | 0 191 | PERMISSIONS 192 | 493 193 | TYPE 194 | 1 195 | UID 196 | 0 197 | 198 | 199 | CHILDREN 200 | 201 | GID 202 | 0 203 | PATH 204 | Input Methods 205 | PATH_TYPE 206 | 0 207 | PERMISSIONS 208 | 493 209 | TYPE 210 | 1 211 | UID 212 | 0 213 | 214 | 215 | CHILDREN 216 | 217 | GID 218 | 0 219 | PATH 220 | Internet Plug-Ins 221 | PATH_TYPE 222 | 0 223 | PERMISSIONS 224 | 493 225 | TYPE 226 | 1 227 | UID 228 | 0 229 | 230 | 231 | CHILDREN 232 | 233 | GID 234 | 0 235 | PATH 236 | LaunchAgents 237 | PATH_TYPE 238 | 0 239 | PERMISSIONS 240 | 493 241 | TYPE 242 | 1 243 | UID 244 | 0 245 | 246 | 247 | CHILDREN 248 | 249 | GID 250 | 0 251 | PATH 252 | LaunchDaemons 253 | PATH_TYPE 254 | 0 255 | PERMISSIONS 256 | 493 257 | TYPE 258 | 1 259 | UID 260 | 0 261 | 262 | 263 | CHILDREN 264 | 265 | GID 266 | 0 267 | PATH 268 | PreferencePanes 269 | PATH_TYPE 270 | 0 271 | PERMISSIONS 272 | 493 273 | TYPE 274 | 1 275 | UID 276 | 0 277 | 278 | 279 | CHILDREN 280 | 281 | GID 282 | 0 283 | PATH 284 | Preferences 285 | PATH_TYPE 286 | 0 287 | PERMISSIONS 288 | 493 289 | TYPE 290 | 1 291 | UID 292 | 0 293 | 294 | 295 | CHILDREN 296 | 297 | GID 298 | 80 299 | PATH 300 | Printers 301 | PATH_TYPE 302 | 0 303 | PERMISSIONS 304 | 493 305 | TYPE 306 | 1 307 | UID 308 | 0 309 | 310 | 311 | CHILDREN 312 | 313 | GID 314 | 0 315 | PATH 316 | PrivilegedHelperTools 317 | PATH_TYPE 318 | 0 319 | PERMISSIONS 320 | 1005 321 | TYPE 322 | 1 323 | UID 324 | 0 325 | 326 | 327 | CHILDREN 328 | 329 | GID 330 | 0 331 | PATH 332 | QuickLook 333 | PATH_TYPE 334 | 0 335 | PERMISSIONS 336 | 493 337 | TYPE 338 | 1 339 | UID 340 | 0 341 | 342 | 343 | CHILDREN 344 | 345 | GID 346 | 0 347 | PATH 348 | QuickTime 349 | PATH_TYPE 350 | 0 351 | PERMISSIONS 352 | 493 353 | TYPE 354 | 1 355 | UID 356 | 0 357 | 358 | 359 | CHILDREN 360 | 361 | GID 362 | 0 363 | PATH 364 | Screen Savers 365 | PATH_TYPE 366 | 0 367 | PERMISSIONS 368 | 493 369 | TYPE 370 | 1 371 | UID 372 | 0 373 | 374 | 375 | CHILDREN 376 | 377 | GID 378 | 0 379 | PATH 380 | Scripts 381 | PATH_TYPE 382 | 0 383 | PERMISSIONS 384 | 493 385 | TYPE 386 | 1 387 | UID 388 | 0 389 | 390 | 391 | CHILDREN 392 | 393 | GID 394 | 0 395 | PATH 396 | Services 397 | PATH_TYPE 398 | 0 399 | PERMISSIONS 400 | 493 401 | TYPE 402 | 1 403 | UID 404 | 0 405 | 406 | 407 | CHILDREN 408 | 409 | GID 410 | 0 411 | PATH 412 | Widgets 413 | PATH_TYPE 414 | 0 415 | PERMISSIONS 416 | 493 417 | TYPE 418 | 1 419 | UID 420 | 0 421 | 422 | 423 | GID 424 | 0 425 | PATH 426 | Library 427 | PATH_TYPE 428 | 0 429 | PERMISSIONS 430 | 493 431 | TYPE 432 | 1 433 | UID 434 | 0 435 | 436 | 437 | CHILDREN 438 | 439 | 440 | CHILDREN 441 | 442 | GID 443 | 0 444 | PATH 445 | Shared 446 | PATH_TYPE 447 | 0 448 | PERMISSIONS 449 | 1023 450 | TYPE 451 | 1 452 | UID 453 | 0 454 | 455 | 456 | GID 457 | 80 458 | PATH 459 | Users 460 | PATH_TYPE 461 | 0 462 | PERMISSIONS 463 | 493 464 | TYPE 465 | 1 466 | UID 467 | 0 468 | 469 | 470 | GID 471 | 0 472 | PATH 473 | / 474 | PATH_TYPE 475 | 0 476 | PERMISSIONS 477 | 493 478 | TYPE 479 | 1 480 | UID 481 | 0 482 | 483 | PAYLOAD_TYPE 484 | 0 485 | PRESERVE_EXTENDED_ATTRIBUTES 486 | 487 | SHOW_INVISIBLE 488 | 489 | SPLIT_FORKS 490 | 491 | TREAT_MISSING_FILES_AS_WARNING 492 | 493 | VERSION 494 | 5 495 | 496 | PACKAGE_SCRIPTS 497 | 498 | POSTINSTALL_PATH 499 | 500 | PATH_TYPE 501 | 0 502 | 503 | PREINSTALL_PATH 504 | 505 | PATH_TYPE 506 | 0 507 | 508 | RESOURCES 509 | 510 | 511 | PACKAGE_SETTINGS 512 | 513 | AUTHENTICATION 514 | 0 515 | CONCLUSION_ACTION 516 | 0 517 | FOLLOW_SYMBOLIC_LINKS 518 | 519 | IDENTIFIER 520 | @MACOS_BUNDLEID@ 521 | LOCATION 522 | 0 523 | NAME 524 | @PROJECT_NAME@ 525 | OVERWRITE_PERMISSIONS 526 | 527 | PAYLOAD_SIZE 528 | -1 529 | REFERENCE_PATH 530 | 531 | RELOCATABLE 532 | 533 | USE_HFS+_COMPRESSION 534 | 535 | VERSION 536 | @PROJECT_VERSION@ 537 | 538 | TYPE 539 | 0 540 | UUID 541 | @MACOS_PACKAGE_UUID@ 542 | 543 | 544 | PROJECT 545 | 546 | PROJECT_COMMENTS 547 | 548 | NOTES 549 | 550 | 551 | 552 | PROJECT_PRESENTATION 553 | 554 | BACKGROUND 555 | 556 | APPAREANCES 557 | 558 | DARK_AQUA 559 | 560 | LIGHT_AQUA 561 | 562 | 563 | SHARED_SETTINGS_FOR_ALL_APPAREANCES 564 | 565 | 566 | INSTALLATION TYPE 567 | 568 | HIERARCHIES 569 | 570 | INSTALLER 571 | 572 | LIST 573 | 574 | 575 | CHILDREN 576 | 577 | DESCRIPTION 578 | 579 | OPTIONS 580 | 581 | HIDDEN 582 | 583 | STATE 584 | 1 585 | 586 | PACKAGE_UUID 587 | @MACOS_PACKAGE_UUID@ 588 | TITLE 589 | 590 | TYPE 591 | 0 592 | UUID 593 | @MACOS_INSTALLER_UUID@ 594 | 595 | 596 | REMOVED 597 | 598 | 599 | 600 | MODE 601 | 0 602 | 603 | INSTALLATION_STEPS 604 | 605 | 606 | ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS 607 | ICPresentationViewIntroductionController 608 | INSTALLER_PLUGIN 609 | Introduction 610 | LIST_TITLE_KEY 611 | InstallerSectionTitle 612 | 613 | 614 | ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS 615 | ICPresentationViewReadMeController 616 | INSTALLER_PLUGIN 617 | ReadMe 618 | LIST_TITLE_KEY 619 | InstallerSectionTitle 620 | 621 | 622 | ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS 623 | ICPresentationViewLicenseController 624 | INSTALLER_PLUGIN 625 | License 626 | LIST_TITLE_KEY 627 | InstallerSectionTitle 628 | 629 | 630 | ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS 631 | ICPresentationViewDestinationSelectController 632 | INSTALLER_PLUGIN 633 | TargetSelect 634 | LIST_TITLE_KEY 635 | InstallerSectionTitle 636 | 637 | 638 | ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS 639 | ICPresentationViewInstallationTypeController 640 | INSTALLER_PLUGIN 641 | PackageSelection 642 | LIST_TITLE_KEY 643 | InstallerSectionTitle 644 | 645 | 646 | ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS 647 | ICPresentationViewInstallationController 648 | INSTALLER_PLUGIN 649 | Install 650 | LIST_TITLE_KEY 651 | InstallerSectionTitle 652 | 653 | 654 | ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS 655 | ICPresentationViewSummaryController 656 | INSTALLER_PLUGIN 657 | Summary 658 | LIST_TITLE_KEY 659 | InstallerSectionTitle 660 | 661 | 662 | INTRODUCTION 663 | 664 | LOCALIZATIONS 665 | 666 | 667 | LICENSE 668 | 669 | LOCALIZATIONS 670 | 671 | MODE 672 | 0 673 | 674 | README 675 | 676 | LOCALIZATIONS 677 | 678 | 679 | SUMMARY 680 | 681 | LOCALIZATIONS 682 | 683 | 684 | TITLE 685 | 686 | LOCALIZATIONS 687 | 688 | 689 | 690 | PROJECT_REQUIREMENTS 691 | 692 | LIST 693 | 694 | 695 | BEHAVIOR 696 | 3 697 | DICTIONARY 698 | 699 | IC_REQUIREMENT_OS_DISK_TYPE 700 | 1 701 | IC_REQUIREMENT_OS_DISTRIBUTION_TYPE 702 | 0 703 | IC_REQUIREMENT_OS_MINIMUM_VERSION 704 | 101300 705 | 706 | IC_REQUIREMENT_CHECK_TYPE 707 | 0 708 | IDENTIFIER 709 | fr.whitebox.Packages.requirement.os 710 | MESSAGE 711 | 712 | NAME 713 | Operating System 714 | STATE 715 | 716 | 717 | 718 | RESOURCES 719 | 720 | ROOT_VOLUME_ONLY 721 | 722 | 723 | PROJECT_SETTINGS 724 | 725 | ADVANCED_OPTIONS 726 | 727 | installer-script.domains:enable_currentUserHome 728 | 1 729 | 730 | BUILD_FORMAT 731 | 0 732 | BUILD_PATH 733 | 734 | PATH 735 | ../@RELATIVE_BUILD_PATH@ 736 | PATH_TYPE 737 | 1 738 | 739 | EXCLUDED_FILES 740 | 741 | 742 | PATTERNS_ARRAY 743 | 744 | 745 | REGULAR_EXPRESSION 746 | 747 | STRING 748 | .DS_Store 749 | TYPE 750 | 0 751 | 752 | 753 | PROTECTED 754 | 755 | PROXY_NAME 756 | Remove .DS_Store files 757 | PROXY_TOOLTIP 758 | Remove ".DS_Store" files created by the Finder. 759 | STATE 760 | 761 | 762 | 763 | PATTERNS_ARRAY 764 | 765 | 766 | REGULAR_EXPRESSION 767 | 768 | STRING 769 | .pbdevelopment 770 | TYPE 771 | 0 772 | 773 | 774 | PROTECTED 775 | 776 | PROXY_NAME 777 | Remove .pbdevelopment files 778 | PROXY_TOOLTIP 779 | Remove ".pbdevelopment" files created by ProjectBuilder or Xcode. 780 | STATE 781 | 782 | 783 | 784 | PATTERNS_ARRAY 785 | 786 | 787 | REGULAR_EXPRESSION 788 | 789 | STRING 790 | CVS 791 | TYPE 792 | 1 793 | 794 | 795 | REGULAR_EXPRESSION 796 | 797 | STRING 798 | .cvsignore 799 | TYPE 800 | 0 801 | 802 | 803 | REGULAR_EXPRESSION 804 | 805 | STRING 806 | .cvspass 807 | TYPE 808 | 0 809 | 810 | 811 | REGULAR_EXPRESSION 812 | 813 | STRING 814 | .svn 815 | TYPE 816 | 1 817 | 818 | 819 | REGULAR_EXPRESSION 820 | 821 | STRING 822 | .git 823 | TYPE 824 | 1 825 | 826 | 827 | REGULAR_EXPRESSION 828 | 829 | STRING 830 | .gitignore 831 | TYPE 832 | 0 833 | 834 | 835 | PROTECTED 836 | 837 | PROXY_NAME 838 | Remove SCM metadata 839 | PROXY_TOOLTIP 840 | Remove helper files and folders used by the CVS, SVN or Git Source Code Management systems. 841 | STATE 842 | 843 | 844 | 845 | PATTERNS_ARRAY 846 | 847 | 848 | REGULAR_EXPRESSION 849 | 850 | STRING 851 | classes.nib 852 | TYPE 853 | 0 854 | 855 | 856 | REGULAR_EXPRESSION 857 | 858 | STRING 859 | designable.db 860 | TYPE 861 | 0 862 | 863 | 864 | REGULAR_EXPRESSION 865 | 866 | STRING 867 | info.nib 868 | TYPE 869 | 0 870 | 871 | 872 | PROTECTED 873 | 874 | PROXY_NAME 875 | Optimize nib files 876 | PROXY_TOOLTIP 877 | Remove "classes.nib", "info.nib" and "designable.nib" files within .nib bundles. 878 | STATE 879 | 880 | 881 | 882 | PATTERNS_ARRAY 883 | 884 | 885 | REGULAR_EXPRESSION 886 | 887 | STRING 888 | Resources Disabled 889 | TYPE 890 | 1 891 | 892 | 893 | PROTECTED 894 | 895 | PROXY_NAME 896 | Remove Resources Disabled folders 897 | PROXY_TOOLTIP 898 | Remove "Resources Disabled" folders. 899 | STATE 900 | 901 | 902 | 903 | SEPARATOR 904 | 905 | 906 | 907 | NAME 908 | @PROJECT_NAME@@PKG_SUFFIX@ 909 | PAYLOAD_ONLY 910 | 911 | TREAT_MISSING_PRESENTATION_DOCUMENTS_AS_WARNING 912 | 913 | 914 | 915 | TYPE 916 | 0 917 | VERSION 918 | 2 919 | 920 | 921 | --------------------------------------------------------------------------------