Initial import from Aitum/obs-vertical-canvas v1.6.4

Forked for Vulcast branding changes.
Licensed under GPL-2.0.
This commit is contained in:
Vulcast 2026-06-29 22:58:49 +01:00
commit 0e83ab9eef
103 changed files with 21652 additions and 0 deletions

17
.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
*~
.DS_Store
/build/
/build_*/
/release/
/installer/Output/
.vscode
.idea
.deps/
# ignore generated files
*.generated.*
**/.Brewfile.lock.json
version.h
cmake/.CMakeBuildNumber
.ccache.conf

97
CMakeLists.txt Normal file
View file

@ -0,0 +1,97 @@
# --- Detect if the plugin is build out of tree or not ---
if(CMAKE_PROJECT_NAME STREQUAL "obs-studio")
set(BUILD_OUT_OF_TREE OFF)
else()
set(BUILD_OUT_OF_TREE ON)
cmake_minimum_required(VERSION 3.16...3.26)
endif()
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/common/bootstrap.cmake" NO_POLICY_SCOPE)
project(${_name} VERSION ${_version})
include(compilerconfig)
include(defaults)
include(helpers)
add_library(${PROJECT_NAME} MODULE)
target_link_libraries(${PROJECT_NAME} PRIVATE OBS::libobs)
if(BUILD_OUT_OF_TREE)
find_package(libobs REQUIRED)
find_package(obs-frontend-api REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE OBS::obs-frontend-api)
else()
target_link_libraries(${PROJECT_NAME} PRIVATE OBS::frontend-api)
endif()
find_package(Qt6 COMPONENTS Widgets Core)
if(BUILD_OUT_OF_TREE)
if(OS_LINUX OR OS_FREEBSD OR OS_OPENBSD)
find_package(Qt6 REQUIRED Gui)
endif()
endif()
target_link_libraries(${PROJECT_NAME} PRIVATE Qt::Core Qt::Widgets)
if((OS_LINUX OR OS_FREEBSD OR OS_OPENBSD) AND Qt6_VERSION VERSION_LESS "6.9.0")
find_package(Qt6 COMPONENTS GuiPrivate)
target_link_libraries(${PROJECT_NAME} PRIVATE Qt::GuiPrivate)
endif()
target_compile_options(
${PROJECT_NAME} PRIVATE $<$<C_COMPILER_ID:Clang,AppleClang>:-Wno-quoted-include-in-framework-header
-Wno-comma>)
set_target_properties(${PROJECT_NAME} PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
set_target_properties(
${PROJECT_NAME}
PROPERTIES AUTOMOC ON
AUTOUIC ON
AUTORCC ON)
if(BUILD_OUT_OF_TREE)
find_package(CURL REQUIRED)
endif()
target_link_libraries(${PROJECT_NAME} PRIVATE CURL::libcurl)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_SOURCE_DIR}/version.h)
if(OS_WINDOWS)
configure_file(cmake/windows/resources/installer-Windows.iss.in "${CMAKE_CURRENT_BINARY_DIR}/installer-Windows.generated.iss")
endif()
target_sources(${PROJECT_NAME} PRIVATE
vertical-canvas.cpp
scenes-dock.cpp
sources-dock.cpp
source-tree.cpp
transitions-dock.cpp
qt-display.cpp
projector.cpp
config-dialog.cpp
hotkey-edit.cpp
name-dialog.cpp
audio-wrapper-source.c
file-updater.c
multi-canvas-source.c
resources.qrc
vertical-canvas.hpp
scenes-dock.hpp
sources-dock.hpp
source-tree.hpp
transitions-dock.hpp
qt-display.hpp
projector.hpp
display-helpers.hpp
config-dialog.hpp
hotkey-edit.hpp
name-dialog.hpp
audio-wrapper-source.h
obs-websocket-api.h
file-updater.h
multi-canvas-source.h)
if(BUILD_OUT_OF_TREE)
set_target_properties_plugin(${CMAKE_PROJECT_NAME} PROPERTIES OUTPUT_NAME ${_name})
else()
set_target_properties_obs(${PROJECT_NAME} PROPERTIES FOLDER "plugins/aitum" PREFIX "")
endif()

190
CMakePresets.json Normal file
View file

@ -0,0 +1,190 @@
{
"version": 3,
"cmakeMinimumRequired": {
"major": 3,
"minor": 22,
"patch": 0
},
"configurePresets": [
{
"name": "template",
"hidden": true,
"cacheVariables": {
"ENABLE_FRONTEND_API": false,
"ENABLE_QT": false
}
},
{
"name": "macos",
"displayName": "macOS Universal",
"description": "Build for macOS (Universal binary)",
"inherits": ["template"],
"binaryDir": "${sourceDir}/build_macos",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
},
"generator": "Xcode",
"warnings": {"dev": true, "deprecated": true},
"cacheVariables": {
"QT_VERSION": "6",
"CMAKE_OSX_DEPLOYMENT_TARGET": "12.0",
"CODESIGN_IDENTITY": "$penv{CODESIGN_IDENT}",
"CODESIGN_TEAM": "$penv{APPLE_TEAM}"
}
},
{
"name": "macos-ci",
"inherits": ["macos"],
"displayName": "macOS Universal CI build",
"description": "Build for macOS (Universal binary) for CI",
"generator": "Xcode",
"cacheVariables": {
"CMAKE_COMPILE_WARNING_AS_ERROR": true
}
},
{
"name": "windows-x64",
"displayName": "Windows x64",
"description": "Build for Windows x64",
"inherits": ["template"],
"binaryDir": "${sourceDir}/build_x64",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"generator": "Visual Studio 17 2022",
"architecture": "x64",
"warnings": {"dev": true, "deprecated": true},
"cacheVariables": {
"QT_VERSION": "6",
"CMAKE_SYSTEM_VERSION": "10.0.18363.657"
}
},
{
"name": "windows-ci-x64",
"inherits": ["windows-x64"],
"displayName": "Windows x64 CI build",
"description": "Build for Windows x64 on CI",
"cacheVariables": {
"CMAKE_COMPILE_WARNING_AS_ERROR": true
}
},
{
"name": "ubuntu-x86_64",
"displayName": "Ubuntu x86_64",
"description": "Build for Ubuntu x86_64",
"inherits": ["template"],
"binaryDir": "${sourceDir}/build_x86_64",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
},
"generator": "Ninja",
"warnings": {"dev": true, "deprecated": true},
"cacheVariables": {
"QT_VERSION": "6",
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
}
},
{
"name": "ubuntu-ci-x86_64",
"inherits": ["ubuntu-x86_64"],
"displayName": "Ubuntu x86_64 CI build",
"description": "Build for Ubuntu x86_64 on CI",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
"CMAKE_COMPILE_WARNING_AS_ERROR": true
}
},
{
"name": "linux-aarch64",
"displayName": "Linux aarch64",
"description": "Build for Linux aarch64",
"inherits": ["template"],
"binaryDir": "${sourceDir}/build_aarch64",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
},
"generator": "Ninja",
"warnings": {"dev": true, "deprecated": true},
"cacheVariables": {
"QT_VERSION": "6",
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
}
},
{
"name": "linux-ci-aarch64",
"inherits": ["linux-aarch64"],
"displayName": "Linux aarch64 CI build",
"description": "Build for Linux aarch64 on CI",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
"CMAKE_COMPILE_WARNING_AS_ERROR": true
}
}
],
"buildPresets": [
{
"name": "macos",
"configurePreset": "macos",
"displayName": "macOS Universal",
"description": "macOS build for Universal architectures",
"configuration": "Release"
},
{
"name": "macos-ci",
"configurePreset": "macos-ci",
"displayName": "macOS Universal CI",
"description": "macOS CI build for Universal architectures",
"configuration": "RelWithDebInfo"
},
{
"name": "windows-x64",
"configurePreset": "windows-x64",
"displayName": "Windows x64",
"description": "Windows build for x64",
"configuration": "RelWithDebInfo"
},
{
"name": "windows-ci-x64",
"configurePreset": "windows-ci-x64",
"displayName": "Windows x64 CI",
"description": "Windows CI build for x64 (RelWithDebInfo configuration)",
"configuration": "RelWithDebInfo"
},
{
"name": "ubuntu-x86_64",
"configurePreset": "ubuntu-x86_64",
"displayName": "Linux x86_64",
"description": "Linux build for x86_64",
"configuration": "RelWithDebInfo"
},
{
"name": "ubuntu-ci-x86_64",
"configurePreset": "ubuntu-ci-x86_64",
"displayName": "Linux x86_64 CI",
"description": "Linux CI build for x86_64",
"configuration": "RelWithDebInfo"
},
{
"name": "linux-aarch64",
"configurePreset": "linux-aarch64",
"displayName": "Linux aarch64",
"description": "Linux build for aarch64",
"configuration": "RelWithDebInfo"
},
{
"name": "linux-ci-aarch64",
"configurePreset": "linux-ci-aarch64",
"displayName": "Linux aarch64 CI",
"description": "Linux CI build for aarch64",
"configuration": "RelWithDebInfo"
}
]
}

339
LICENSE Normal file
View file

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

28
README.md Normal file
View file

@ -0,0 +1,28 @@
# Vertical Canvas for OBS Studio
Plugin for [OBS Studio](https://github.com/obsproject/obs-studio) to add vertical canvas by [![Aitum logo](media/aitum.png) Aitum](https://aitum.tv)
# Support
Currently supported distributions can be found at:
* Aitum Website: https://aitum.tv/vertical
* GitHub: https://github.com/Aitum/obs-vertical-canvas
* Flatpak: https://github.com/flathub/com.obsproject.Studio.Plugin.VerticalCanvas
For support, please see our troubleshooter: https://l.aitum.tv/vh
If this does not solve your issue, please use the relevant channel on our [Discord server](https://aitum.tv/discord).
# Build
- In-tree build
- Build OBS Studio: https://obsproject.com/wiki/Install-Instructions
- Check out this repository to UI/frontend-plugins/vertical-canvas
- Add `add_subdirectory(vertical-canvas)` to UI/frontend-plugins/CMakeLists.txt
- Rebuild OBS Studio
- Stand-alone build
- Verify that you have development files for OBS
- Check out this repository and run `cmake -S . -B build -DBUILD_OUT_OF_TREE=On && cmake --build build`
# Translations
Please read [Translations](TRANSLATIONS.md)

8
TRANSLATIONS.md Normal file
View file

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

100
audio-wrapper-source.c Normal file
View file

@ -0,0 +1,100 @@
#include <obs.h>
#include "audio-wrapper-source.h"
const char *audio_wrapper_get_name(void *type_data)
{
UNUSED_PARAMETER(type_data);
return "transition_audio_wrapper";
}
void *audio_wrapper_create(obs_data_t *settings, obs_source_t *source)
{
UNUSED_PARAMETER(settings);
struct audio_wrapper_info *audio_wrapper = bzalloc(sizeof(struct audio_wrapper_info));
audio_wrapper->source = source;
return audio_wrapper;
}
void audio_wrapper_destroy(void *data)
{
bfree(data);
}
bool audio_wrapper_render(void *data, uint64_t *ts_out, struct obs_source_audio_mix *audio, uint32_t mixers, size_t channels,
size_t sample_rate)
{
UNUSED_PARAMETER(sample_rate);
struct audio_wrapper_info *aw = (struct audio_wrapper_info *)data;
obs_source_t *source = aw->target(aw->param);
if (!source)
return false;
uint64_t timestamp = obs_source_get_audio_timestamp(source);
if (!timestamp) {
obs_source_release(source);
return false;
}
if (!aw->mixers) {
*ts_out = timestamp;
obs_source_release(source);
return true;
}
mixers &= aw->mixers(aw->param);
if (mixers == 0) {
*ts_out = timestamp;
obs_source_release(source);
return true;
}
struct obs_source_audio_mix child_audio;
obs_source_get_audio_mix(source, &child_audio);
for (size_t mix = 0; mix < MAX_AUDIO_MIXES; mix++) {
if ((mixers & (1 << mix)) == 0)
continue;
for (size_t ch = 0; ch < channels; ch++) {
float *out = audio->output[mix].data[ch];
float *in = child_audio.output[mix].data[ch];
float *end = in + AUDIO_OUTPUT_FRAMES;
while (in < end)
*(out++) += *(in++);
}
}
*ts_out = timestamp;
obs_source_release(source);
return true;
}
static void audio_wrapper_enum_sources(void *data, obs_source_enum_proc_t enum_callback, void *param, bool active)
{
UNUSED_PARAMETER(active);
struct audio_wrapper_info *aw = (struct audio_wrapper_info *)data;
obs_source_t *source = aw->target(aw->param);
if (!source)
return;
enum_callback(aw->source, source, param);
obs_source_release(source);
}
void audio_wrapper_enum_active_sources(void *data, obs_source_enum_proc_t enum_callback, void *param)
{
audio_wrapper_enum_sources(data, enum_callback, param, true);
}
void audio_wrapper_enum_all_sources(void *data, obs_source_enum_proc_t enum_callback, void *param)
{
audio_wrapper_enum_sources(data, enum_callback, param, false);
}
struct obs_source_info audio_wrapper_source = {
.id = "vertical_audio_wrapper_source",
.type = OBS_SOURCE_TYPE_SCENE,
.output_flags = OBS_SOURCE_COMPOSITE | OBS_SOURCE_CAP_DISABLED,
.get_name = audio_wrapper_get_name,
.create = audio_wrapper_create,
.destroy = audio_wrapper_destroy,
.audio_render = audio_wrapper_render,
.enum_active_sources = audio_wrapper_enum_active_sources,
.enum_all_sources = audio_wrapper_enum_all_sources,
};

17
audio-wrapper-source.h Normal file
View file

@ -0,0 +1,17 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
struct audio_wrapper_info {
obs_source_t *source;
void *param;
obs_source_t *(*target)(void *param);
uint32_t (*mixers)(void *param);
};
extern struct obs_source_info audio_wrapper_source;
#ifdef __cplusplus
};
#endif

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

272
build-aux/.run-format.zsh Normal file
View file

@ -0,0 +1,272 @@
#!/usr/bin/env zsh
builtin emulate -L zsh
setopt EXTENDED_GLOB
setopt PUSHD_SILENT
setopt ERR_EXIT
setopt ERR_RETURN
setopt NO_UNSET
setopt PIPE_FAIL
setopt NO_AUTO_PUSHD
setopt NO_PUSHD_IGNORE_DUPS
setopt FUNCTION_ARGZERO
## Enable for script debugging
# setopt WARN_CREATE_GLOBAL
# setopt WARN_NESTED_VAR
# setopt XTRACE
autoload -Uz is-at-least && if ! is-at-least 5.8; then
print -u2 -PR "%F{1}${funcstack[1]##*/}:%f Running on Zsh version %B${ZSH_VERSION}%b, but Zsh %B5.2%b is the minimum supported version. Upgrade zsh to fix this issue."
exit 1
fi
invoke_formatter() {
if (( # < 1 )) {
log_error "Usage invoke_formatter [formatter_name]"
exit 2
}
local formatter="${1}"
shift
local -a source_files=(${@})
case ${formatter} {
clang)
if (( ${+commands[clang-format-17]} )) {
local formatter=clang-format-17
} elif (( ${+commands[clang-format]} )) {
local formatter=clang-format
} else {
log_error "No viable clang-format version found (required 17.0.3)"
exit 2
}
local -a formatter_version=($(${formatter} --version))
if ! is-at-least 17.0.3 ${formatter_version[-1]}; then
log_error "clang-format is not version 17.0.3 or above (found ${formatter_version[-1]}."
exit 2
fi
if ! is-at-least ${formatter_version[-1]} 17.0.3; then
log_error "clang-format is more recent than version 17.0.3 (found ${formatter_version[-1]})."
exit 2
fi
if (( ! #source_files )) source_files=(src/**/*.(c|cpp|h|hpp|m|mm)(.N))
local -a format_args=(-style=file -fallback-style=none)
if (( _loglevel > 2 )) format_args+=(--verbose)
check_files() {
local -i num_failures=0
local -a source_files=($@)
local file
local -a format_args=(-style=file -fallback-style=none)
if (( _loglevel > 2 )) format_args+=(--verbose)
local -a command=(${formatter} ${format_args})
for file (${source_files}) {
if ! ${command} "${file}" | diff -q "${file}" - &> /dev/null; then
log_error "${file} requires formatting changes."
if (( fail_on_error == 2 )) return 2;
num_failures=$(( num_failures + 1 ))
fi
}
if (( num_failures && fail_on_error == 1 )) return 2
}
format_files() {
local -a source_files=($@)
if (( ${#source_files} )) {
local -a format_args=(-style=file -fallback-style=none -i)
if (( _loglevel > 2 )) format_args+=(--verbose)
"${formatter}" ${format_args} ${source_files}
}
}
;;
gersemi)
local formatter=gersemi
if (( ${+commands[gersemi]} )) {
local gersemi_version=($(gersemi --version))
if ! is-at-least 0.12.0 ${gersemi_version[2]}; then
log_error "gersemi is not version 0.12.0 or above (found ${gersemi_version[2]}."
exit 2
fi
}
if (( ! #source_files )) source_files=(CMakeLists.txt (cmake)/**/(CMakeLists.txt|*.cmake)(.N))
check_files() {
local -i num_failures=0
local -a source_files=($@)
local file
local -a command=(${formatter} -c --no-cache ${source_files})
if (( ${#source_files} )) {
while read -r line; do
local -a line_tokens=(${(z)line})
if (( #line_tokens )) {
file=${line_tokens[1]//*${project_root}\//}
log_error "${file} requires formatting changes."
} else {
log_error "${line}"
}
if (( fail_on_error == 2 )) return 2
num_failures=$(( num_failures + 1 ))
done < <(${command} 2>&1)
if (( num_failures && fail_on_error == 1 )) return 2
}
}
format_files() {
local -a source_files=($@)
if (( ${#source_files} )) {
"${formatter}" -i ${source_files}
}
}
;;
swift)
local formatter=swift-format
if (( ${+commands[swift-format]} )) {
local swift_format_version=$(swift-format --version)
if ! is-at-least 508.0.0 ${swift_format_version}; then
log_error "swift-format is not version 508.0.0 or above (found ${swift_format_version})."
exit 2
fi
} else {
log_error "No viable swift-format version found (required 508.0.0)"
exit 2
}
if (( ! #source_files )) source_files=(src/**/*.swift(.N))
check_files() {
local -i num_failures=0
local -a source_files=($@)
local file
local -a format_args=()
local -a command=(${formatter} ${format_args})
for file (${source_files}) {
if ! "${command}" "${file}" | diff -q "${file}" - &> /dev/null; then
log_error "${file} requires formatting changes."
if (( fail_on_error == 2 )) return 2;
num_failures=$(( num_failures + 1 ))
fi
}
if (( num_failures && fail_on_error == 1 )) return 2
}
format_files() {
local -a source_files=($@)
if (( ${#source_files} )) {
local -a format_args=(-i)
"${formatter}" ${format_args} ${source_files}
}
}
;;
*) log_error "Invalid formatter specified: ${1}. Valid options are clang-format, gersemi, and swift-format."; exit 2 ;;
}
local file
local -i num_failures=0
if (( check_only )) {
if (( ${+functions[check_files]} )) {
check_files ${source_files}
} else {
log_error "No format check function defined for formatter '${formatter}'"
exit 2
}
} else {
if (( ${+functions[format_files]} )) {
format_files ${source_files}
} else {
log_error "No format function defined for formatter '${formatter}'"
exit 2
}
}
}
run_format() {
if (( ! ${+SCRIPT_HOME} )) typeset -g SCRIPT_HOME=${ZSH_ARGZERO:A:h}
if (( ! ${+FORMATTER_NAME} )) typeset -g FORMATTER_NAME=${${(s:-:)ZSH_ARGZERO:t:r}[2]}
local project_root=${SCRIPT_HOME:A:h}
typeset -g host_os=${${(L)$(uname -s)}//darwin/macos}
local -i fail_on_error=0
local -i check_only=0
local -i verbosity=1
local -r _version='2.0.0'
fpath=("${SCRIPT_HOME}/.functions" ${fpath})
autoload -Uz log_info log_error log_output set_loglevel log_status log_warning
local -r _usage="
Usage: %B${functrace[1]%:*}%b <option> [SOURCE_FILES]
%BOptions%b:
%F{yellow} Formatting options%f
-----------------------------------------------------------------------------
%B-c | --check%b Check only, no actual formatting takes place
%F{yellow} Output options%f
-----------------------------------------------------------------------------
%B-v | --verbose%b Verbose (more detailed output)
%B--fail-[never|error] Fail script never/on formatting change - default: %B%F{green}never%f%b
%B--debug%b Debug (very detailed and added output)
%F{yellow} General options%f
-----------------------------------------------------------------------------
%B-h | --help%b Print this usage help
%B-V | --version%b Print script version information"
local -a args
while (( # )) {
case ${1} {
-c|--check) check_only=1; shift ;;
-v|--verbose) (( verbosity += 1 )); shift ;;
-h|--help) log_output ${_usage}; exit 0 ;;
-V|--version) print -Pr "${_version}"; exit 0 ;;
--debug) verbosity=3; shift ;;
--fail-never)
fail_on_error=0
shift
;;
--fail-error)
fail_on_error=1
shift
;;
--fail-fast)
fail_on_error=2
shift
;;
*)
args+=($@)
break
;;
}
}
set -- ${(@)args}
set_loglevel ${verbosity}
invoke_formatter ${FORMATTER_NAME} ${args}
}
run_format ${@}

1
build-aux/run-clang-format Symbolic link
View file

@ -0,0 +1 @@
.run-format.zsh

1
build-aux/run-cmake-format Symbolic link
View file

@ -0,0 +1 @@
.run-format.zsh

1
build-aux/run-gersemi Symbolic link
View file

@ -0,0 +1 @@
.run-format.zsh

1
build-aux/run-swift-format Symbolic link
View file

@ -0,0 +1 @@
.run-format.zsh

48
buildspec.json Normal file
View file

@ -0,0 +1,48 @@
{
"dependencies": {
"obs-studio": {
"version": "31.1.0",
"baseUrl": "https://github.com/obsproject/obs-studio/archive/refs/tags",
"label": "OBS sources",
"hashes": {
"macos": "ba903f3848cfabd9ea1205aea14b69e83a2eb43ad6f3f7cc80a6a900bce0e56e",
"windows-x64": "ca4b7513bb0b34c708f1cc89016f6ed879b710c912a93387e9f83445c1fc68bb"
}
},
"prebuilt": {
"version": "2025-05-23",
"baseUrl": "https://github.com/obsproject/obs-deps/releases/download",
"label": "Pre-Built obs-deps",
"hashes": {
"macos": "7b1448581b92c6f8d4e2f9b1b10eb318ebfc4df7255638e05bacb49b620e190d",
"windows-x64": "88dc7bab50542b4b58194cdd5d7e6f4b73cde7c0d1ee7a040a6d9c9df05187b1"
}
},
"qt6": {
"version": "2025-05-23",
"baseUrl": "https://github.com/obsproject/obs-deps/releases/download",
"label": "Pre-Built Qt6",
"hashes": {
"macos": "d4945fe719d31d353dc76298f58823d7891126e33af5029f763427ea9f80bfaa",
"windows-x64": "bd80383dc799c0bba2f7fae793613b3fbd2cc3a3712b3d3b63b490721d187d9e"
},
"debugSymbols": {
"windows-x64": "9c9a15c5dbe59eb6a1c31c06b7208f3c4403671fbdfaef341cac3beb7af24ea1"
}
}
},
"platformConfig": {
"macos": {
"bundleId": "tv.aitum.vertical"
}
},
"name": "vertical-canvas",
"displayName": "Vertical",
"version": "1.6.4",
"author": "Aitum",
"website": "https://aitum.tv",
"email": "support@aitum.tv",
"uuids": {
"windowsApp": "9072EA15-785B-4BD9-8310-68CEECDA2117"
}
}

View file

@ -0,0 +1,82 @@
# Plugin bootstrap module
include_guard(GLOBAL)
# Map fallback configurations for optimized build configurations
# gersemi: off
set(
CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO
RelWithDebInfo
Release
MinSizeRel
None
""
)
set(
CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL
MinSizeRel
Release
RelWithDebInfo
None
""
)
set(
CMAKE_MAP_IMPORTED_CONFIG_RELEASE
Release
RelWithDebInfo
MinSizeRel
None
""
)
# gersemi: on
# Add common module directories to default search path
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/common")
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/buildspec.json" buildspec)
string(JSON _name GET ${buildspec} name)
string(JSON _website GET ${buildspec} website)
string(JSON _author GET ${buildspec} author)
string(JSON _email GET ${buildspec} email)
string(JSON _version GET ${buildspec} version)
string(JSON _bundleId GET ${buildspec} platformConfig macos bundleId)
string(JSON _uuidApp GET ${buildspec} uuids windowsApp)
set(PLUGIN_AUTHOR ${_author})
set(PLUGIN_WEBSITE ${_website})
set(PLUGIN_EMAIL ${_email})
set(PLUGIN_VERSION ${_version})
set(MACOS_BUNDLEID ${_bundleId})
set(UUID_APP ${_uuidApp})
string(REPLACE "." ";" _version_canonical "${_version}")
list(GET _version_canonical 0 PLUGIN_VERSION_MAJOR)
list(GET _version_canonical 1 PLUGIN_VERSION_MINOR)
list(GET _version_canonical 2 PLUGIN_VERSION_PATCH)
unset(_version_canonical)
include(buildnumber)
include(osconfig)
# Allow selection of common build types via UI
if(NOT CMAKE_GENERATOR MATCHES "(Xcode|Visual Studio .+)")
if(NOT CMAKE_BUILD_TYPE)
set(
CMAKE_BUILD_TYPE
"RelWithDebInfo"
CACHE STRING
"OBS build type [Release, RelWithDebInfo, Debug, MinSizeRel]"
FORCE
)
set_property(
CACHE CMAKE_BUILD_TYPE
PROPERTY STRINGS Release RelWithDebInfo Debug MinSizeRel
)
endif()
endif()
# Disable exports automatically going into the CMake package registry
set(CMAKE_EXPORT_PACKAGE_REGISTRY FALSE)
# Enable default inclusion of targets' source and binary directory
set(CMAKE_INCLUDE_CURRENT_DIR TRUE)

View file

@ -0,0 +1,30 @@
# CMake build number module
include_guard(GLOBAL)
# Define build number cache file
set(
_BUILD_NUMBER_CACHE
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/.CMakeBuildNumber"
CACHE INTERNAL
"OBS build number cache file"
)
# Read build number from cache file or manual override
if(NOT DEFINED PLUGIN_BUILD_NUMBER)
if(EXISTS "${_BUILD_NUMBER_CACHE}")
file(READ "${_BUILD_NUMBER_CACHE}" PLUGIN_BUILD_NUMBER)
math(EXPR PLUGIN_BUILD_NUMBER "${PLUGIN_BUILD_NUMBER}+1")
else()
if("$ENV{CI}")
if("$ENV{GITHUB_RUN_ID}")
set(PLUGIN_BUILD_NUMBER "$ENV{GITHUB_RUN_ID}")
elseif("$ENV{GITLAB_RUN_ID}")
set(PLUGIN_BUILD_NUMBER "$ENV{GITLAB_RUN_ID}")
else()
set(PLUGIN_BUILD_NUMBER "1")
endif()
endif()
endif()
file(WRITE "${_BUILD_NUMBER_CACHE}" "${PLUGIN_BUILD_NUMBER}")
endif()

View file

@ -0,0 +1,227 @@
# Common build dependencies module
include_guard(GLOBAL)
# _check_deps_version: Checks for obs-deps VERSION file in prefix paths
function(_check_deps_version version)
set(found FALSE)
foreach(path IN LISTS CMAKE_PREFIX_PATH)
if(EXISTS "${path}/share/obs-deps/VERSION")
if(dependency STREQUAL qt6 AND NOT EXISTS "${path}/lib/cmake/Qt6/Qt6Config.cmake")
set(found FALSE)
continue()
endif()
file(READ "${path}/share/obs-deps/VERSION" _check_version)
string(REPLACE "\n" "" _check_version "${_check_version}")
string(REPLACE "-" "." _check_version "${_check_version}")
string(REPLACE "-" "." version "${version}")
if(_check_version VERSION_EQUAL version)
set(found TRUE)
break()
elseif(_check_version VERSION_LESS version)
message(
AUTHOR_WARNING
"Older ${label} version detected in ${path}: \n"
"Found ${_check_version}, require ${version}"
)
list(REMOVE_ITEM CMAKE_PREFIX_PATH "${path}")
list(APPEND CMAKE_PREFIX_PATH "${path}")
set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH})
continue()
else()
message(
AUTHOR_WARNING
"Newer ${label} version detected in ${path}: \n"
"Found ${_check_version}, require ${version}"
)
set(found TRUE)
break()
endif()
endif()
endforeach()
return(PROPAGATE found CMAKE_PREFIX_PATH)
endfunction()
# _setup_obs_studio: Create obs-studio build project, then build libobs and obs-frontend-api
function(_setup_obs_studio)
if(NOT libobs_DIR)
set(_is_fresh --fresh)
endif()
if(OS_WINDOWS)
set(_cmake_generator "${CMAKE_GENERATOR}")
set(_cmake_arch "-A ${arch},version=${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}")
set(_cmake_extra "-DCMAKE_SYSTEM_VERSION=${CMAKE_SYSTEM_VERSION} -DCMAKE_ENABLE_SCRIPTING=OFF")
elseif(OS_MACOS)
set(_cmake_generator "Xcode")
set(_cmake_arch "-DCMAKE_OSX_ARCHITECTURES:STRING='arm64;x86_64'")
set(_cmake_extra "-DCMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif()
message(STATUS "Configure ${label} (${arch})")
execute_process(
COMMAND
"${CMAKE_COMMAND}" -S "${dependencies_dir}/${_obs_destination}" -B
"${dependencies_dir}/${_obs_destination}/build_${arch}" -G ${_cmake_generator} "${_cmake_arch}"
-DOBS_CMAKE_VERSION:STRING=3.0.0 -DENABLE_PLUGINS:BOOL=OFF -DENABLE_FRONTEND:BOOL=OFF
-DOBS_VERSION_OVERRIDE:STRING=${_obs_version} "-DCMAKE_PREFIX_PATH='${CMAKE_PREFIX_PATH}'" ${_is_fresh}
${_cmake_extra}
RESULT_VARIABLE _process_result
COMMAND_ERROR_IS_FATAL ANY
OUTPUT_QUIET
)
message(STATUS "Configure ${label} (${arch}) - done")
message(STATUS "Build ${label} (Debug - ${arch})")
execute_process(
COMMAND "${CMAKE_COMMAND}" --build build_${arch} --target obs-frontend-api --config Debug --parallel
WORKING_DIRECTORY "${dependencies_dir}/${_obs_destination}"
RESULT_VARIABLE _process_result
COMMAND_ERROR_IS_FATAL ANY
OUTPUT_QUIET
)
message(STATUS "Build ${label} (Debug - ${arch}) - done")
message(STATUS "Build ${label} (Release - ${arch})")
execute_process(
COMMAND "${CMAKE_COMMAND}" --build build_${arch} --target obs-frontend-api --config Release --parallel
WORKING_DIRECTORY "${dependencies_dir}/${_obs_destination}"
RESULT_VARIABLE _process_result
COMMAND_ERROR_IS_FATAL ANY
OUTPUT_QUIET
)
message(STATUS "Build ${label} (Reelase - ${arch}) - done")
message(STATUS "Install ${label} (${arch})")
execute_process(
COMMAND
"${CMAKE_COMMAND}" --install build_${arch} --component Development --config Debug --prefix "${dependencies_dir}"
WORKING_DIRECTORY "${dependencies_dir}/${_obs_destination}"
RESULT_VARIABLE _process_result
COMMAND_ERROR_IS_FATAL ANY
OUTPUT_QUIET
)
execute_process(
COMMAND
"${CMAKE_COMMAND}" --install build_${arch} --component Development --config Release --prefix "${dependencies_dir}"
WORKING_DIRECTORY "${dependencies_dir}/${_obs_destination}"
RESULT_VARIABLE _process_result
COMMAND_ERROR_IS_FATAL ANY
OUTPUT_QUIET
)
message(STATUS "Install ${label} (${arch}) - done")
endfunction()
# _check_dependencies: Fetch and extract pre-built OBS build dependencies
function(_check_dependencies)
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/buildspec.json" buildspec)
string(JSON dependency_data GET ${buildspec} dependencies)
foreach(dependency IN LISTS dependencies_list)
string(JSON data GET ${dependency_data} ${dependency})
string(JSON version GET ${data} version)
string(JSON hash GET ${data} hashes ${platform})
string(JSON url GET ${data} baseUrl)
string(JSON label GET ${data} label)
string(JSON revision ERROR_VARIABLE error GET ${data} revision ${platform})
message(STATUS "Setting up ${label} (${arch})")
set(file "${${dependency}_filename}")
set(destination "${${dependency}_destination}")
string(REPLACE "VERSION" "${version}" file "${file}")
string(REPLACE "VERSION" "${version}" destination "${destination}")
string(REPLACE "ARCH" "${arch}" file "${file}")
string(REPLACE "ARCH" "${arch}" destination "${destination}")
if(revision)
string(REPLACE "_REVISION" "_v${revision}" file "${file}")
string(REPLACE "-REVISION" "-v${revision}" file "${file}")
else()
string(REPLACE "_REVISION" "" file "${file}")
string(REPLACE "-REVISION" "" file "${file}")
endif()
if(EXISTS "${dependencies_dir}/.dependency_${dependency}_${arch}.sha256")
file(
READ
"${dependencies_dir}/.dependency_${dependency}_${arch}.sha256"
OBS_DEPENDENCY_${dependency}_${arch}_HASH
)
endif()
set(skip FALSE)
if(dependency STREQUAL prebuilt OR dependency STREQUAL qt6)
if(OBS_DEPENDENCY_${dependency}_${arch}_HASH STREQUAL ${hash})
_check_deps_version(${version})
if(found)
set(skip TRUE)
endif()
endif()
endif()
if(skip)
message(STATUS "Setting up ${label} (${arch}) - skipped")
continue()
endif()
if(dependency STREQUAL obs-studio)
set(url ${url}/${file})
else()
set(url ${url}/${version}/${file})
endif()
if(NOT EXISTS "${dependencies_dir}/${file}")
message(STATUS "Downloading ${url}")
file(DOWNLOAD "${url}" "${dependencies_dir}/${file}" STATUS download_status EXPECTED_HASH SHA256=${hash})
list(GET download_status 0 error_code)
list(GET download_status 1 error_message)
if(error_code GREATER 0)
message(STATUS "Downloading ${url} - Failure")
message(FATAL_ERROR "Unable to download ${url}, failed with error: ${error_message}")
file(REMOVE "${dependencies_dir}/${file}")
else()
message(STATUS "Downloading ${url} - done")
endif()
endif()
if(NOT OBS_DEPENDENCY_${dependency}_${arch}_HASH STREQUAL ${hash})
file(REMOVE_RECURSE "${dependencies_dir}/${destination}")
endif()
if(NOT EXISTS "${dependencies_dir}/${destination}")
file(MAKE_DIRECTORY "${dependencies_dir}/${destination}")
if(dependency STREQUAL obs-studio)
file(ARCHIVE_EXTRACT INPUT "${dependencies_dir}/${file}" DESTINATION "${dependencies_dir}")
else()
file(ARCHIVE_EXTRACT INPUT "${dependencies_dir}/${file}" DESTINATION "${dependencies_dir}/${destination}")
endif()
endif()
file(WRITE "${dependencies_dir}/.dependency_${dependency}_${arch}.sha256" "${hash}")
if(dependency STREQUAL prebuilt)
list(APPEND CMAKE_PREFIX_PATH "${dependencies_dir}/${destination}")
elseif(dependency STREQUAL qt6)
list(APPEND CMAKE_PREFIX_PATH "${dependencies_dir}/${destination}")
elseif(dependency STREQUAL obs-studio)
set(_obs_version ${version})
set(_obs_destination "${destination}")
list(APPEND CMAKE_PREFIX_PATH "${dependencies_dir}")
endif()
message(STATUS "Setting up ${label} (${arch}) - done")
endforeach()
list(REMOVE_DUPLICATES CMAKE_PREFIX_PATH)
set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} CACHE PATH "CMake prefix search path" FORCE)
_setup_obs_studio()
endfunction()

25
cmake/common/ccache.cmake Normal file
View file

@ -0,0 +1,25 @@
# OBS CMake ccache module
include_guard(GLOBAL)
if(NOT DEFINED CCACHE_PROGRAM)
message(DEBUG "Trying to find ccache on build host")
find_program(CCACHE_PROGRAM "ccache")
mark_as_advanced(CCACHE_PROGRAM)
endif()
if(CCACHE_PROGRAM)
message(DEBUG "Trying to find ccache on build host - done")
message(DEBUG "Ccache found as ${CCACHE_PROGRAM}")
option(ENABLE_CCACHE "Enable compiler acceleration with ccache" OFF)
if(ENABLE_CCACHE)
set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
set(CMAKE_OBJC_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
set(CMAKE_OBJCXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
set(CMAKE_CUDA_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
endif()
else()
message(DEBUG "Trying to find ccache on build host - skipped")
endif()

View file

@ -0,0 +1,83 @@
# CMake common compiler options module
include_guard(GLOBAL)
# Set C and C++ language standards to C17 and C++17
set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED TRUE)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
# Set symbols to be hidden by default for C and C++
set(CMAKE_C_VISIBILITY_PRESET hidden)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN TRUE)
# clang options for C, C++, ObjC, and ObjC++
set(
_obs_clang_common_options
-fno-strict-aliasing
-Wno-trigraphs
-Wno-missing-field-initializers
-Wno-missing-prototypes
-Werror=return-type
-Wunreachable-code
-Wquoted-include-in-framework-header
-Wno-missing-braces
-Wparentheses
-Wswitch
-Wno-unused-function
-Wno-unused-label
-Wunused-parameter
-Wunused-variable
-Wunused-value
-Wempty-body
-Wuninitialized
-Wno-unknown-pragmas
-Wfour-char-constants
-Wconstant-conversion
-Wno-conversion
-Wint-conversion
-Wbool-conversion
-Wenum-conversion
-Wnon-literal-null-conversion
-Wsign-compare
-Wshorten-64-to-32
-Wpointer-sign
-Wnewline-eof
-Wno-implicit-fallthrough
-Wdeprecated-declarations
-Wno-sign-conversion
-Winfinite-recursion
-Wcomma
-Wno-strict-prototypes
-Wno-semicolon-before-method-body
-Wformat-security
-Wvla
-Wno-error=shorten-64-to-32
)
# clang options for C
set(_obs_clang_c_options ${_obs_clang_common_options} -Wno-shadow -Wno-float-conversion)
# clang options for C++
set(
_obs_clang_cxx_options
${_obs_clang_common_options}
-Wno-non-virtual-dtor
-Wno-overloaded-virtual
-Wno-exit-time-destructors
-Wno-shadow
-Winvalid-offsetof
-Wmove
-Werror=block-capture-autoreleasing
-Wrange-loop-analysis
)
if(CMAKE_CXX_STANDARD GREATER_EQUAL 20)
list(APPEND _obs_clang_cxx_options -fno-char8_t)
endif()
if(NOT DEFINED CMAKE_COMPILE_WARNING_AS_ERROR)
set(CMAKE_COMPILE_WARNING_AS_ERROR ON)
endif()

View file

@ -0,0 +1,49 @@
# CMake common helper functions module
include_guard(GLOBAL)
# check_uuid: Helper function to check for valid UUID
function(check_uuid uuid_string return_value)
set(valid_uuid TRUE)
# gersemi: off
set(uuid_token_lengths 8 4 4 4 12)
# gersemi: on
set(token_num 0)
string(REPLACE "-" ";" uuid_tokens ${uuid_string})
list(LENGTH uuid_tokens uuid_num_tokens)
if(uuid_num_tokens EQUAL 5)
message(DEBUG "UUID ${uuid_string} is valid with 5 tokens.")
foreach(uuid_token IN LISTS uuid_tokens)
list(GET uuid_token_lengths ${token_num} uuid_target_length)
string(LENGTH "${uuid_token}" uuid_actual_length)
if(uuid_actual_length EQUAL uuid_target_length)
string(REGEX MATCH "[0-9a-fA-F]+" uuid_hex_match ${uuid_token})
if(NOT uuid_hex_match STREQUAL uuid_token)
set(valid_uuid FALSE)
break()
endif()
else()
set(valid_uuid FALSE)
break()
endif()
math(EXPR token_num "${token_num}+1")
endforeach()
else()
set(valid_uuid FALSE)
endif()
message(DEBUG "UUID ${uuid_string} valid: ${valid_uuid}")
set(${return_value} ${valid_uuid} PARENT_SCOPE)
endfunction()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/plugin-support.c.in")
configure_file(src/plugin-support.c.in plugin-support.c @ONLY)
add_library(plugin-support STATIC)
target_sources(plugin-support PRIVATE plugin-support.c PUBLIC src/plugin-support.h)
target_include_directories(plugin-support PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src")
if(OS_LINUX OR OS_FREEBSD OR OS_OPENBSD)
# add fPIC on Linux to prevent shared object errors
set_property(TARGET plugin-support PROPERTY POSITION_INDEPENDENT_CODE ON)
endif()
endif()

View file

@ -0,0 +1,20 @@
# CMake operating system bootstrap module
include_guard(GLOBAL)
if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
set(CMAKE_C_EXTENSIONS FALSE)
set(CMAKE_CXX_EXTENSIONS FALSE)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/windows")
set(OS_WINDOWS TRUE)
elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin")
set(CMAKE_C_EXTENSIONS FALSE)
set(CMAKE_CXX_EXTENSIONS FALSE)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos")
set(OS_MACOS TRUE)
elseif(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux|FreeBSD|OpenBSD")
set(CMAKE_CXX_EXTENSIONS FALSE)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/linux")
string(TOUPPER "${CMAKE_HOST_SYSTEM_NAME}" _SYSTEM_NAME_U)
set(OS_${_SYSTEM_NAME_U} TRUE)
endif()

View file

@ -0,0 +1,78 @@
# CMake Linux compiler configuration module
include_guard(GLOBAL)
include(ccache)
include(compiler_common)
option(ENABLE_COMPILER_TRACE "Enable Clang time-trace (required Clang and Ninja)" OFF)
mark_as_advanced(ENABLE_COMPILER_TRACE)
# gcc options for C
set(
_obs_gcc_c_options
-fno-strict-aliasing
-fopenmp-simd
-Wdeprecated-declarations
-Wempty-body
-Wenum-conversion
-Werror=return-type
-Wextra
-Wformat
-Wformat-security
-Wno-conversion
-Wno-float-conversion
-Wno-implicit-fallthrough
-Wno-missing-braces
-Wno-missing-field-initializers
-Wno-shadow
-Wno-sign-conversion
-Wno-trigraphs
-Wno-unknown-pragmas
-Wno-unused-function
-Wno-unused-label
-Wparentheses
-Wuninitialized
-Wunreachable-code
-Wunused-parameter
-Wunused-value
-Wunused-variable
-Wvla
)
add_compile_options(
-fopenmp-simd
"$<$<COMPILE_LANG_AND_ID:C,GNU>:${_obs_gcc_c_options}>"
"$<$<COMPILE_LANG_AND_ID:C,GNU>:-Wint-conversion;-Wno-missing-prototypes;-Wno-strict-prototypes;-Wpointer-sign>"
"$<$<COMPILE_LANG_AND_ID:CXX,GNU>:${_obs_gcc_c_options}>"
"$<$<COMPILE_LANG_AND_ID:CXX,GNU>:-Winvalid-offsetof;-Wno-overloaded-virtual>"
"$<$<COMPILE_LANG_AND_ID:C,Clang>:${_obs_clang_c_options}>"
"$<$<COMPILE_LANG_AND_ID:CXX,Clang>:${_obs_clang_cxx_options}>"
)
if(CMAKE_CXX_COMPILER_ID STREQUAL GNU)
# * Disable false-positive warning in GCC 12.1.0 and later
# * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105562
if(CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 12.1.0)
add_compile_options(-Wno-error=maybe-uninitialized)
endif()
# * Add warning for infinite recursion (added in GCC 12)
# * Also disable warnings for stringop-overflow due to https://gcc.gnu.org/bugzilla/show_bug.cgi?id=106297
if(CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 12.0.0)
add_compile_options(-Winfinite-recursion -Wno-stringop-overflow)
endif()
if(CMAKE_SYSTEM_PROCESSOR STREQUAL aarch64)
add_compile_options(-Wno-error=type-limits)
endif()
endif()
# Enable compiler and build tracing (requires Ninja generator)
if(ENABLE_COMPILER_TRACE AND CMAKE_GENERATOR STREQUAL "Ninja")
add_compile_options($<$<COMPILE_LANG_AND_ID:C,Clang>:-ftime-trace> $<$<COMPILE_LANG_AND_ID:CXX,Clang>:-ftime-trace>)
else()
set(ENABLE_COMPILER_TRACE OFF CACHE STRING "Enable Clang time-trace (required Clang and Ninja)" FORCE)
endif()
add_compile_definitions($<$<CONFIG:DEBUG>:DEBUG> $<$<CONFIG:DEBUG>:_DEBUG> SIMDE_ENABLE_OPENMP)

View file

@ -0,0 +1,88 @@
# CMake Linux defaults module
include_guard(GLOBAL)
# Set default installation directories
include(GNUInstallDirs)
if(CMAKE_INSTALL_LIBDIR MATCHES "(CMAKE_SYSTEM_PROCESSOR)")
string(REPLACE "CMAKE_SYSTEM_PROCESSOR" "${CMAKE_SYSTEM_PROCESSOR}" CMAKE_INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}")
endif()
# Enable find_package targets to become globally available targets
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL TRUE)
set(CPACK_PACKAGE_NAME "${CMAKE_PROJECT_NAME}")
set(CPACK_PACKAGE_VERSION "${CMAKE_PROJECT_VERSION}")
set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CMAKE_C_LIBRARY_ARCHITECTURE}")
set(CPACK_GENERATOR "DEB")
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${PLUGIN_EMAIL}")
set(CPACK_SET_DESTDIR ON)
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.25.0 OR NOT CMAKE_CROSSCOMPILING)
set(CPACK_DEBIAN_DEBUGINFO_PACKAGE ON)
endif()
set(CPACK_OUTPUT_FILE_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/release")
set(CPACK_SOURCE_GENERATOR "TXZ")
set(
CPACK_SOURCE_IGNORE_FILES
".*~$"
\\.git/
\\.github/
\\.gitignore
\\.ccache/
build_.*
cmake/\\.CMakeBuildNumber
release/
)
set(CPACK_VERBATIM_VARIABLES YES)
set(CPACK_SOURCE_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-source")
set(CPACK_ARCHIVE_THREADS 0)
include(CPack)
find_package(libobs QUIET)
if(NOT TARGET OBS::libobs)
find_package(LibObs REQUIRED)
add_library(OBS::libobs ALIAS libobs)
if(ENABLE_FRONTEND_API)
find_path(
obs-frontend-api_INCLUDE_DIR
NAMES obs-frontend-api.h
PATHS /usr/include /usr/local/include
PATH_SUFFIXES obs
)
find_library(obs-frontend-api_LIBRARY NAMES obs-frontend-api PATHS /usr/lib /usr/local/lib)
if(obs-frontend-api_LIBRARY)
if(NOT TARGET OBS::obs-frontend-api)
if(IS_ABSOLUTE "${obs-frontend-api_LIBRARY}")
add_library(OBS::obs-frontend-api UNKNOWN IMPORTED)
set_property(TARGET OBS::obs-frontend-api PROPERTY IMPORTED_LOCATION "${obs-frontend-api_LIBRARY}")
else()
add_library(OBS::obs-frontend-api INTERFACE IMPORTED)
set_property(TARGET OBS::obs-frontend-api PROPERTY IMPORTED_LIBNAME "${obs-frontend-api_LIBRARY}")
endif()
set_target_properties(
OBS::obs-frontend-api
PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${obs-frontend-api_INCLUDE_DIR}"
)
endif()
endif()
endif()
macro(find_package)
if(NOT "${ARGV0}" STREQUAL libobs AND NOT "${ARGV0}" STREQUAL obs-frontend-api)
_find_package(${ARGV})
endif()
endmacro()
endif()

106
cmake/linux/helpers.cmake Normal file
View file

@ -0,0 +1,106 @@
# CMake Linux helper functions module
include_guard(GLOBAL)
include(helpers_common)
# set_target_properties_plugin: Set target properties for use in obs-studio
function(set_target_properties_plugin target)
set(options "")
set(oneValueArgs "")
set(multiValueArgs PROPERTIES)
cmake_parse_arguments(PARSE_ARGV 0 _STPO "${options}" "${oneValueArgs}" "${multiValueArgs}")
message(DEBUG "Setting additional properties for target ${target}...")
while(_STPO_PROPERTIES)
list(POP_FRONT _STPO_PROPERTIES key value)
set_property(TARGET ${target} PROPERTY ${key} "${value}")
endwhile()
set_target_properties(
${target}
PROPERTIES VERSION ${PLUGIN_VERSION} SOVERSION ${PLUGIN_VERSION_MAJOR} PREFIX ""
)
install(
TARGETS ${target}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/obs-plugins
)
if(TARGET plugin-support)
target_link_libraries(${target} PRIVATE plugin-support)
endif()
add_custom_command(
TARGET ${target}
POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$<CONFIG>"
COMMAND
"${CMAKE_COMMAND}" -E copy_if_different "$<TARGET_FILE:${target}>" "${CMAKE_CURRENT_BINARY_DIR}/rundir/$<CONFIG>"
COMMENT "Copy ${target} to rundir"
VERBATIM
)
target_install_resources(${target})
get_target_property(target_sources ${target} SOURCES)
set(target_ui_files ${target_sources})
list(FILTER target_ui_files INCLUDE REGEX ".+\\.(ui|qrc)")
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "UI Files" FILES ${target_ui_files})
endfunction()
# Helper function to add resources into bundle
function(target_install_resources target)
message(DEBUG "Installing resources for target ${target}...")
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/data")
file(GLOB_RECURSE data_files "${CMAKE_CURRENT_SOURCE_DIR}/data/*")
foreach(data_file IN LISTS data_files)
cmake_path(
RELATIVE_PATH
data_file
BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/"
OUTPUT_VARIABLE relative_path
)
cmake_path(GET relative_path PARENT_PATH relative_path)
target_sources(${target} PRIVATE "${data_file}")
source_group("Resources/${relative_path}" FILES "${data_file}")
endforeach()
install(
DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/"
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/obs/obs-plugins/${target}
USE_SOURCE_PERMISSIONS
)
add_custom_command(
TARGET ${target}
POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$<CONFIG>/${target}"
COMMAND
"${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/data"
"${CMAKE_CURRENT_BINARY_DIR}/rundir/$<CONFIG>/${target}"
COMMENT "Copy ${target} resources to rundir"
VERBATIM
)
endif()
endfunction()
# Helper function to add a specific resource to a bundle
function(target_add_resource target resource)
message(DEBUG "Add resource '${resource}' to target ${target} at destination '${target_destination}'...")
install(FILES "${resource}" DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/obs/obs-plugins/${target})
add_custom_command(
TARGET ${target}
POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$<CONFIG>/${target}"
COMMAND "${CMAKE_COMMAND}" -E copy "${resource}" "${CMAKE_CURRENT_BINARY_DIR}/rundir/$<CONFIG>/${target}"
COMMENT "Copy ${target} resource ${resource} to rundir"
VERBATIM
)
source_group("Resources" FILES "${resource}")
endfunction()

View file

@ -0,0 +1,56 @@
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_CROSSCOMPILING TRUE)
set(CMAKE_C_COMPILER /usr/bin/clang)
set(CMAKE_CXX_COMPILER /usr/bin/clang++)
set(CMAKE_C_COMPILER_TARGET aarch64-linux-gnu)
set(CMAKE_CXX_COMPILER_TARGET aarch64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(PKG_CONFIG_EXECUTABLE
/usr/bin/aarch64-linux-gnu-pkg-config
CACHE FILEPATH "pkg-config executable")
execute_process(
COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-ranlib
OUTPUT_VARIABLE CMAKE_RANLIB
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(
COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-ar
OUTPUT_VARIABLE CMAKE_LLVM_AR
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(
COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-readelf
OUTPUT_VARIABLE READELF
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(
COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-objcopy
OUTPUT_VARIABLE CMAKE_LLVM_OBJCOPY
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(
COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-objdump
OUTPUT_VARIABLE CMAKE_LLVM_OBJDUMP
OUTPUT_STRIP_TRAILING_WHITESPACE)
set(CMAKE_AR
"${CMAKE_LLVM_AR}"
CACHE INTERNAL "${CMAKE_SYSTEM_NAME} ar" FORCE)
set(CMAKE_OBJCOPY
"${CMAKE_LLVM_OBJCOPY}"
CACHE INTERNAL "${CMAKE_SYSTEM_NAME} objcopy" FORCE)
set(CMAKE_OBJDUMP
"${CMAKE_LLVM_OBJDUMP}"
CACHE INTERNAL "${CMAKE_SYSTEM_NAME} objdump" FORCE)
set(CPACK_READELF_EXECUTABLE "${READELF}")
set(CPACK_OBJCOPY_EXECUTABLE "${CMAKE_LLVM_OBJCOPY}")
set(CPACK_OBJDUMP_EXECUTABLE "${CMAKE_LLVM_OBJDUMP}")
set(CPACK_PACKAGE_ARCHITECTURE arm64)
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE arm64)

View file

@ -0,0 +1,20 @@
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_CROSSCOMPILING TRUE)
set(CMAKE_C_COMPILER /usr/bin/aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/aarch64-linux-gnu-g++)
set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(PKG_CONFIG_EXECUTABLE
/usr/bin/aarch64-linux-gnu-pkg-config
CACHE FILEPATH "pkg-config executable")
set(CPACK_READELF_EXECUTABLE /usr/bin/aarch64-linux-gnu-readelf)
set(CPACK_OBJCOPY_EXECUTABLE /usr/bin/aarch64-linux-gnu-objcopy)
set(CPACK_OBJDUMP_EXECUTABLE /usr/bin/aarch64-linux-gnu-objdump)
set(CPACK_PACKAGE_ARCHITECTURE arm64)
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE arm64)

View file

@ -0,0 +1,56 @@
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_CROSSCOMPILING TRUE)
set(CMAKE_C_COMPILER /usr/bin/clang)
set(CMAKE_CXX_COMPILER /usr/bin/clang++)
set(CMAKE_C_COMPILER_TARGET x86_64-linux-gnu)
set(CMAKE_CXX_COMPILER_TARGET x86_64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(PKG_CONFIG_EXECUTABLE
/usr/bin/x86_64-linux-gnu-pkg-config
CACHE FILEPATH "pkg-config executable")
execute_process(
COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-ranlib
OUTPUT_VARIABLE CMAKE_RANLIB
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(
COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-ar
OUTPUT_VARIABLE CMAKE_LLVM_AR
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(
COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-readelf
OUTPUT_VARIABLE READELF
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(
COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-objcopy
OUTPUT_VARIABLE CMAKE_LLVM_OBJCOPY
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(
COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-objdump
OUTPUT_VARIABLE CMAKE_LLVM_OBJDUMP
OUTPUT_STRIP_TRAILING_WHITESPACE)
set(CMAKE_AR
"${CMAKE_LLVM_AR}"
CACHE INTERNAL "${CMAKE_SYSTEM_NAME} ar" FORCE)
set(CMAKE_OBJCOPY
"${CMAKE_LLVM_OBJCOPY}"
CACHE INTERNAL "${CMAKE_SYSTEM_NAME} objcopy" FORCE)
set(CMAKE_OBJDUMP
"${CMAKE_LLVM_OBJDUMP}"
CACHE INTERNAL "${CMAKE_SYSTEM_NAME} objdump" FORCE)
set(CPACK_READELF_EXECUTABLE "${READELF}")
set(CPACK_OBJCOPY_EXECUTABLE "${CMAKE_LLVM_OBJCOPY}")
set(CPACK_OBJDUMP_EXECUTABLE "${CMAKE_LLVM_OBJDUMP}")
set(CPACK_PACKAGE_ARCHITECTURE x86_64)
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE x86_64)

View file

@ -0,0 +1,20 @@
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_CROSSCOMPILING TRUE)
set(CMAKE_C_COMPILER /usr/bin/x86_64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/x86_64-linux-gnu-g++)
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(PKG_CONFIG_EXECUTABLE
/usr/bin/x86_64-linux-gnu-pkg-config
CACHE FILEPATH "pkg-config executable")
set(CPACK_READELF_EXECUTABLE /usr/bin/x86_64-linux-gnu-readelf)
set(CPACK_OBJCOPY_EXECUTABLE /usr/bin/x86_64-linux-gnu-objcopy)
set(CPACK_OBJDUMP_EXECUTABLE /usr/bin/x86_64-linux-gnu-objdump)
set(CPACK_PACKAGE_ARCHITECTURE x86_64)
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE x86_64)

View file

@ -0,0 +1,35 @@
# CMake macOS build dependencies module
include_guard(GLOBAL)
include(buildspec_common)
# _check_dependencies_macos: Set up macOS slice for _check_dependencies
function(_check_dependencies_macos)
set(arch universal)
set(platform macos)
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/buildspec.json" buildspec)
set(dependencies_dir "${CMAKE_CURRENT_SOURCE_DIR}/.deps")
set(prebuilt_filename "macos-deps-VERSION-ARCH_REVISION.tar.xz")
set(prebuilt_destination "obs-deps-VERSION-ARCH")
set(qt6_filename "macos-deps-qt6-VERSION-ARCH-REVISION.tar.xz")
set(qt6_destination "obs-deps-qt6-VERSION-ARCH")
set(obs-studio_filename "VERSION.tar.gz")
set(obs-studio_destination "obs-studio-VERSION")
set(dependencies_list prebuilt qt6 obs-studio)
_check_dependencies()
execute_process(
COMMAND "xattr" -r -d com.apple.quarantine "${dependencies_dir}"
RESULT_VARIABLE result
COMMAND_ERROR_IS_FATAL ANY
)
list(APPEND CMAKE_FRAMEWORK_PATH "${dependencies_dir}/Frameworks")
set(CMAKE_FRAMEWORK_PATH ${CMAKE_FRAMEWORK_PATH} PARENT_SCOPE)
endfunction()
_check_dependencies_macos()

View file

@ -0,0 +1,109 @@
# OBS CMake macOS compiler configuration module
include_guard(GLOBAL)
option(ENABLE_COMPILER_TRACE "Enable clang time-trace" OFF)
mark_as_advanced(ENABLE_COMPILER_TRACE)
if(NOT XCODE)
message(FATAL_ERROR "Building OBS Studio on macOS requires Xcode generator.")
endif()
include(ccache)
include(compiler_common)
add_compile_options("$<$<NOT:$<COMPILE_LANGUAGE:Swift>>:-fopenmp-simd>")
# Enable selection between arm64 and x86_64 targets
if(NOT CMAKE_OSX_ARCHITECTURES)
set(CMAKE_OSX_ARCHITECTURES arm64 CACHE STRING "Build architectures for macOS" FORCE)
endif()
set_property(CACHE CMAKE_OSX_ARCHITECTURES PROPERTY STRINGS arm64 x86_64)
# Ensure recent enough Xcode and platform SDK
function(check_sdk_requirements)
set(obs_macos_minimum_sdk 15.0) # Keep in sync with Xcode
set(obs_macos_minimum_xcode 16.0) # Keep in sync with SDK
execute_process(
COMMAND xcrun --sdk macosx --show-sdk-platform-version
OUTPUT_VARIABLE obs_macos_current_sdk
RESULT_VARIABLE result
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(NOT result EQUAL 0)
message(
FATAL_ERROR
"Failed to fetch macOS SDK version. "
"Ensure that the macOS SDK is installed and that xcode-select points at the Xcode developer directory."
)
endif()
message(DEBUG "macOS SDK version: ${obs_macos_current_sdk}")
if(obs_macos_current_sdk VERSION_LESS obs_macos_minimum_sdk)
message(
FATAL_ERROR
"Your macOS SDK version (${obs_macos_current_sdk}) is too low. "
"The macOS ${obs_macos_minimum_sdk} SDK (Xcode ${obs_macos_minimum_xcode}) is required to build OBS."
)
endif()
execute_process(COMMAND xcrun --find xcodebuild OUTPUT_VARIABLE obs_macos_xcodebuild RESULT_VARIABLE result)
if(NOT result EQUAL 0)
message(
FATAL_ERROR
"Xcode was not found. "
"Ensure you have installed Xcode and that xcode-select points at the Xcode developer directory."
)
endif()
message(DEBUG "Path to xcodebuild binary: ${obs_macos_xcodebuild}")
if(XCODE_VERSION VERSION_LESS obs_macos_minimum_xcode)
message(
FATAL_ERROR
"Your Xcode version (${XCODE_VERSION}) is too low. Xcode ${obs_macos_minimum_xcode} is required to build OBS."
)
endif()
endfunction()
check_sdk_requirements()
# Enable dSYM generator for release builds
string(APPEND CMAKE_C_FLAGS_RELEASE " -g")
string(APPEND CMAKE_CXX_FLAGS_RELEASE " -g")
string(APPEND CMAKE_OBJC_FLAGS_RELEASE " -g")
string(APPEND CMAKE_OBJCXX_FLAGS_RELEASE " -g")
# Default ObjC compiler options used by Xcode:
#
# * -Wno-implicit-atomic-properties
# * -Wno-objc-interface-ivars
# * -Warc-repeated-use-of-weak
# * -Wno-arc-maybe-repeated-use-of-weak
# * -Wimplicit-retain-self
# * -Wduplicate-method-match
# * -Wshadow
# * -Wfloat-conversion
# * -Wobjc-literal-conversion
# * -Wno-selector
# * -Wno-strict-selector-match
# * -Wundeclared-selector
# * -Wdeprecated-implementations
# * -Wprotocol
# * -Werror=block-capture-autoreleasing
# * -Wrange-loop-analysis
# Default ObjC++ compiler options used by Xcode:
#
# * -Wno-non-virtual-dtor
add_compile_definitions(
$<$<NOT:$<COMPILE_LANGUAGE:Swift>>:$<$<CONFIG:DEBUG>:DEBUG>>
$<$<NOT:$<COMPILE_LANGUAGE:Swift>>:$<$<CONFIG:DEBUG>:_DEBUG>>
$<$<NOT:$<COMPILE_LANGUAGE:Swift>>:SIMDE_ENABLE_OPENMP>
)
if(ENABLE_COMPILER_TRACE)
add_compile_options(
$<$<NOT:$<COMPILE_LANGUAGE:Swift>>:-ftime-trace>
"$<$<COMPILE_LANGUAGE:Swift>:SHELL:-Xfrontend -debug-time-expression-type-checking>"
"$<$<COMPILE_LANGUAGE:Swift>:SHELL:-Xfrontend -debug-time-function-bodies>"
)
add_link_options(LINKER:-print_statistics)
endif()

View file

@ -0,0 +1,39 @@
# CMake macOS defaults module
include_guard(GLOBAL)
# Set empty codesigning team if not specified as cache variable
if(NOT CODESIGN_TEAM)
set(CODESIGN_TEAM "" CACHE STRING "OBS code signing team for macOS" FORCE)
# Set ad-hoc codesigning identity if not specified as cache variable
if(NOT CODESIGN_IDENTITY)
set(CODESIGN_IDENTITY "-" CACHE STRING "OBS code signing identity for macOS" FORCE)
endif()
endif()
include(xcode)
include(buildspec)
# Use Applications directory as default install destination
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(
CMAKE_INSTALL_PREFIX
"$ENV{HOME}/Library/Application Support/obs-studio/plugins"
CACHE STRING
"Default plugin installation directory"
FORCE
)
endif()
# Enable find_package targets to become globally available targets
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL TRUE)
# Enable RPATH support for generated binaries
set(CMAKE_MACOSX_RPATH TRUE)
# Use RPATHs from build tree _in_ the build tree
set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)
# Do not add default linker search paths to RPATH
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH FALSE)
# Use common bundle-relative RPATH for installed targets
set(CMAKE_INSTALL_RPATH "@executable_path/../Frameworks")

102
cmake/macos/helpers.cmake Normal file
View file

@ -0,0 +1,102 @@
# CMake macOS helper functions module
include_guard(GLOBAL)
include(helpers_common)
# set_target_properties_obs: Set target properties for use in obs-studio
function(set_target_properties_plugin target)
set(options "")
set(oneValueArgs "")
set(multiValueArgs PROPERTIES)
cmake_parse_arguments(PARSE_ARGV 0 _STPO "${options}" "${oneValueArgs}" "${multiValueArgs}")
message(DEBUG "Setting additional properties for target ${target}...")
while(_STPO_PROPERTIES)
list(POP_FRONT _STPO_PROPERTIES key value)
set_property(TARGET ${target} PROPERTY ${key} "${value}")
endwhile()
string(TIMESTAMP CURRENT_YEAR "%Y")
set_target_properties(
${target}
PROPERTIES
BUNDLE TRUE
BUNDLE_EXTENSION plugin
XCODE_ATTRIBUTE_PRODUCT_NAME ${target}
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER ${MACOS_BUNDLEID}
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION ${PLUGIN_BUILD_NUMBER}
XCODE_ATTRIBUTE_MARKETING_VERSION ${PLUGIN_VERSION}
XCODE_ATTRIBUTE_GENERATE_INFOPLIST_FILE YES
XCODE_ATTRIBUTE_INFOPLIST_FILE ""
XCODE_ATTRIBUTE_INFOPLIST_KEY_CFBundleDisplayName ${target}
XCODE_ATTRIBUTE_INFOPLIST_KEY_NSHumanReadableCopyright "(c) ${CURRENT_YEAR} ${PLUGIN_AUTHOR}"
XCODE_ATTRIBUTE_INSTALL_PATH "$(USER_LIBRARY_DIR)/Application Support/obs-studio/plugins"
)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/entitlements.plist")
set_target_properties(
${target}
PROPERTIES XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/entitlements.plist"
)
endif()
if(TARGET plugin-support)
target_link_libraries(${target} PRIVATE plugin-support)
endif()
target_install_resources(${target})
add_custom_command(
TARGET ${target}
POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$<CONFIG>"
COMMAND
"${CMAKE_COMMAND}" -E copy_directory "$<TARGET_BUNDLE_DIR:${target}>"
"${CMAKE_CURRENT_BINARY_DIR}/rundir/$<CONFIG>/$<TARGET_BUNDLE_DIR_NAME:${target}>"
COMMENT "Copy ${target} to rundir"
VERBATIM
)
get_target_property(target_sources ${target} SOURCES)
set(target_ui_files ${target_sources})
list(FILTER target_ui_files INCLUDE REGEX ".+\\.(ui|qrc)")
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "UI Files" FILES ${target_ui_files})
install(TARGETS ${target} LIBRARY DESTINATION .)
install(FILES "$<TARGET_BUNDLE_DIR:${target}>.dsym" CONFIGURATIONS Release DESTINATION . OPTIONAL)
configure_file(cmake/macos/resources/distribution.in "${CMAKE_CURRENT_BINARY_DIR}/distribution" @ONLY)
configure_file(cmake/macos/resources/create-package.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/create-package.cmake" @ONLY)
install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/create-package.cmake")
endfunction()
# target_install_resources: Helper function to add resources into bundle
function(target_install_resources target)
message(DEBUG "Installing resources for target ${target}...")
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/data")
file(GLOB_RECURSE data_files "${CMAKE_CURRENT_SOURCE_DIR}/data/*")
list(FILTER data_files EXCLUDE REGEX "\\.DS_Store$")
foreach(data_file IN LISTS data_files)
cmake_path(
RELATIVE_PATH
data_file
BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/"
OUTPUT_VARIABLE relative_path
)
cmake_path(GET relative_path PARENT_PATH relative_path)
target_sources(${target} PRIVATE "${data_file}")
set_property(SOURCE "${data_file}" PROPERTY MACOSX_PACKAGE_LOCATION "Resources/${relative_path}")
source_group("Resources/${relative_path}" FILES "${data_file}")
endforeach()
endif()
endfunction()
# target_add_resource: Helper function to add a specific resource to a bundle
function(target_add_resource target resource)
message(DEBUG "Add resource ${resource} to target ${target} at destination ${destination}...")
target_sources(${target} PRIVATE "${resource}")
set_property(SOURCE "${resource}" PROPERTY MACOSX_PACKAGE_LOCATION Resources)
source_group("Resources" FILES "${resource}")
endfunction()

View file

@ -0,0 +1,26 @@
#!/bin/sh
if [[ "$1" == "${CMAKE_C_COMPILER}" ]] ; then
shift
fi
export CCACHE_DIR='${CMAKE_SOURCE_DIR}/.ccache'
export CCACHE_MAXSIZE='1G'
export CCACHE_CPP2=true
export CCACHE_DEPEND=true
export CCACHE_DIRECT=true
export CCACHE_FILECLONE=true
export CCACHE_INODECACHE=true
export CCACHE_COMPILERCHECK='content'
CCACHE_SLOPPINESS='file_stat_matches,include_file_mtime,include_file_ctime,system_headers'
if [[ "${CMAKE_C_COMPILER_ID}" == "AppleClang" ]]; then
CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS},modules,clang_index_store"
fi
export CCACHE_SLOPPINESS
if [[ "${CI}" ]]; then
export CCACHE_NOHASHDIR=true
fi
exec "${CMAKE_C_COMPILER_LAUNCHER}" "${CMAKE_C_COMPILER}" "$@"

View file

@ -0,0 +1,26 @@
#!/bin/sh
if [[ "$1" == "${CMAKE_CXX_COMPILER}" ]] ; then
shift
fi
export CCACHE_DIR='${CMAKE_SOURCE_DIR}/.ccache'
export CCACHE_MAXSIZE='1G'
export CCACHE_CPP2=true
export CCACHE_DEPEND=true
export CCACHE_DIRECT=true
export CCACHE_FILECLONE=true
export CCACHE_INODECACHE=true
export CCACHE_COMPILERCHECK='content'
CCACHE_SLOPPINESS='file_stat_matches,include_file_mtime,include_file_ctime,system_headers'
if [[ "${CMAKE_C_COMPILER_ID}" == "AppleClang" ]]; then
CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS},modules,clang_index_store"
fi
export CCACHE_SLOPPINESS
if [[ "${CI}" ]]; then
export CCACHE_NOHASHDIR=true
fi
exec "${CMAKE_CXX_COMPILER_LAUNCHER}" "${CMAKE_CXX_COMPILER}" "$@"

View file

@ -0,0 +1,35 @@
make_directory("$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/package/Library/Application Support/obs-studio/plugins")
if(EXISTS "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin" AND NOT IS_SYMLINK "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin")
file(INSTALL DESTINATION "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/package/Library/Application Support/obs-studio/plugins"
TYPE DIRECTORY FILES "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin" USE_SOURCE_PERMISSIONS)
if(CMAKE_INSTALL_CONFIG_NAME MATCHES "^([Rr][Ee][Ll][Ee][Aa][Ss][Ee])$" OR CMAKE_INSTALL_CONFIG_NAME MATCHES "^([Mm][Ii][Nn][Ss][Ii][Zz][Ee][Rr][Ee][Ll])$")
if(EXISTS "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin.dSYM" AND NOT IS_SYMLINK "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin.dSYM")
file(INSTALL DESTINATION "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/package/Library/Application Support/obs-studio/plugins" TYPE DIRECTORY FILES "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin.dSYM" USE_SOURCE_PERMISSIONS)
endif()
endif()
endif()
make_directory("$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/temp")
execute_process(
COMMAND /usr/bin/pkgbuild
--identifier '@MACOS_BUNDLEID@'
--version '@CMAKE_PROJECT_VERSION@'
--root "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/package"
"$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/temp/@CMAKE_PROJECT_NAME@.pkg"
COMMAND_ERROR_IS_FATAL ANY
)
execute_process(
COMMAND /usr/bin/productbuild
--distribution "@CMAKE_CURRENT_BINARY_DIR@/distribution"
--package-path "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/temp"
"$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.pkg"
COMMAND_ERROR_IS_FATAL ANY)
if(EXISTS "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.pkg")
file(REMOVE_RECURSE "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/temp")
file(REMOVE_RECURSE "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/package")
endif()

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<installer-gui-script minSpecVersion="1.0">
<options
rootVolumeOnly="true"
hostArchitectures="arm64,x86_64"
customize="never"
allow-external-scripts="no" />
<domains enable_currentUserHome="true" enable_anywhere="false" enable_localSystem="false" />
<title>@CMAKE_PROJECT_NAME@</title>
<choices-outline>
<line choice="obs-plugin" />
</choices-outline>
<choice id="obs-plugin" title="@CMAKE_PROJECT_NAME@" description="">
<pkg-ref id="@MACOS_BUNDLEID@" />
</choice>
<pkg-ref id="@MACOS_BUNDLEID@" version="@CMAKE_PROJECT_VERSION@">#@CMAKE_PROJECT_NAME@.pkg</pkg-ref>
<installation-check script="installCheck();" />
<script>
function installCheck() {
var macOSVersion = system.version.ProductVersion
if (system.compareVersions(macOSVersion, '@CMAKE_OSX_DEPLOYMENT_TARGET@') == -1) {
my.result.title = system.localizedStandardStringWithFormat(
'InstallationCheckError',
system.localizedString('DISTRIBUTION_TITLE')
);
my.result.message = ' ';
my.result.type = 'Fatal';
return false;
}
}
</script>
</installer-gui-script>

View file

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

174
cmake/macos/xcode.cmake Normal file
View file

@ -0,0 +1,174 @@
# CMake macOS Xcode module
include_guard(GLOBAL)
set(CMAKE_XCODE_GENERATE_SCHEME TRUE)
# Use a compiler wrapper to enable ccache in Xcode projects
if(ENABLE_CCACHE AND CCACHE_PROGRAM)
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/resources/ccache-launcher-c.in" ccache-launcher-c)
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/resources/ccache-launcher-cxx.in" ccache-launcher-cxx)
execute_process(
COMMAND chmod a+rx "${CMAKE_CURRENT_BINARY_DIR}/ccache-launcher-c" "${CMAKE_CURRENT_BINARY_DIR}/ccache-launcher-cxx"
)
set(CMAKE_XCODE_ATTRIBUTE_CC "${CMAKE_CURRENT_BINARY_DIR}/ccache-launcher-c")
set(CMAKE_XCODE_ATTRIBUTE_CXX "${CMAKE_CURRENT_BINARY_DIR}/ccache-launcher-cxx")
set(CMAKE_XCODE_ATTRIBUTE_LD "${CMAKE_C_COMPILER}")
set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_CXX_COMPILER}")
endif()
# Set project variables
set(CMAKE_XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION ${PLUGIN_BUILD_NUMBER})
set(CMAKE_XCODE_ATTRIBUTE_DYLIB_COMPATIBILITY_VERSION 1.0.0)
set(CMAKE_XCODE_ATTRIBUTE_MARKETING_VERSION ${PLUGIN_VERSION})
# Set deployment target
set(CMAKE_XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET ${CMAKE_OSX_DEPLOYMENT_TARGET})
if(NOT CODESIGN_TEAM)
# Switch to manual codesigning if no codesigning team is provided
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_STYLE Manual)
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "${CODESIGN_IDENTITY}")
else()
if(CODESIGN_IDENTITY AND NOT CODESIGN_IDENTITY STREQUAL "-")
# Switch to manual codesigning if a non-adhoc codesigning identity is provided
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_STYLE Manual)
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "${CODESIGN_IDENTITY}")
else()
# Switch to automatic codesigning via valid team ID
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_STYLE Automatic)
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Development")
endif()
set(CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "${CODESIGN_TEAM}")
endif()
# Only create a single Xcode project file
set(CMAKE_XCODE_GENERATE_TOP_LEVEL_PROJECT_ONLY TRUE)
# Add all libraries to project link phase (lets Xcode handle linking)
set(CMAKE_XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION)
# Enable codesigning with secure timestamp when not in Debug configuration (required for Notarization)
set(CMAKE_XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS[variant=Release] "--timestamp")
set(CMAKE_XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS[variant=RelWithDebInfo] "--timestamp")
set(CMAKE_XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS[variant=MinSizeRel] "--timestamp")
# Enable codesigning with hardened runtime option when not in Debug configuration (required for Notarization)
set(CMAKE_XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME[variant=Release] YES)
set(CMAKE_XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME[variant=RelWithDebInfo] YES)
set(CMAKE_XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME[variant=MinSizeRel] YES)
# Disable injection of Xcode's base entitlements used for debugging when not in Debug configuration (required for
# Notarization)
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_INJECT_BASE_ENTITLEMENTS[variant=Release] NO)
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_INJECT_BASE_ENTITLEMENTS[variant=RelWithDebInfo] NO)
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_INJECT_BASE_ENTITLEMENTS[variant=MinSizeRel] NO)
# Use Swift version 5.0 by default
set(CMAKE_XCODE_ATTRIBUTE_SWIFT_VERSION 5.0)
# Use DWARF with separate dSYM files when in Release or MinSizeRel configuration.
#
# * Currently overruled by CMake's Xcode generator, requires adding '-g' flag to raw compiler command line for desired
# output configuration. Report to KitWare.
#
set(CMAKE_XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT[variant=Debug] dwarf)
set(CMAKE_XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT[variant=RelWithDebInfo] dwarf)
set(CMAKE_XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT[variant=Release] dwarf-with-dsym)
set(CMAKE_XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT[variant=MinSizeRel] dwarf-with-dsym)
# Make all symbols hidden by default (currently overriden by CMake's compiler flags)
set(CMAKE_XCODE_ATTRIBUTE_GCC_SYMBOLS_PRIVATE_EXTERN YES)
set(CMAKE_XCODE_ATTRIBUTE_GCC_INLINES_ARE_PRIVATE_EXTERN YES)
# Strip unused code
set(CMAKE_XCODE_ATTRIBUTE_DEAD_CODE_STRIPPING YES)
# Build active architecture only in Debug configuration
set(CMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH[variant=Debug] YES)
# Enable testability in Debug configuration
set(CMAKE_XCODE_ATTRIBUTE_ENABLE_TESTABILITY[variant=Debug] YES)
# Enable using ARC in ObjC by default
set(CMAKE_XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC YES)
# Enable weak references in manual retain release
set(CMAKE_XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_WEAK YES)
# Disable strict aliasing
set(CMAKE_XCODE_ATTRIBUTE_GCC_STRICT_ALIASING NO)
# Set C++ language default to c17
#
# * CMake explicitly sets the version via compiler flag when transitive dependencies require specific compiler feature
# set, resulting in the flag being added twice. Report to KitWare as a feature request for Xcode generator
# * See also: https://gitlab.kitware.com/cmake/cmake/-/issues/17183
#
# set(CMAKE_XCODE_ATTRIBUTE_GCC_C_LANGUAGE_STANDARD c17)
#
# Set C++ language default to c++17
#
# * See above. Report to KitWare as a feature request for Xcode generator
#
# set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LANGUAGE_STANDARD c++17)
# Enable support for module imports in ObjC
set(CMAKE_XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES YES)
# Enable automatic linking of imported modules in ObjC
set(CMAKE_XCODE_ATTRIBUTE_CLANG_MODULES_AUTOLINK YES)
# Enable strict msg_send rules for ObjC
set(CMAKE_XCODE_ATTRIBUTE_ENABLE_STRICT_OBJC_MSGSEND YES)
# Set default warnings for ObjC and C++
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING YES_ERROR)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_BOOL_CONVERSION YES)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS YES)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_COMMA YES)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_CONSTANT_CONVERSION YES)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_EMPTY_BODY YES)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_ENUM_CONVERSION YES)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_INFINITE_RECURSION YES)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_INT_CONVERSION YES)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_NON_LITERAL_NULL_CONVERSION YES)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF YES)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_OBJC_LITERAL_CONVERSION YES)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK YES)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER YES)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_RANGE_LOOP_ANALYSIS YES)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_STRICT_PROTOTYPES NO)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION NO)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_SUSPICIOUS_MOVE YES)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_UNREACHABLE_CODE YES)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN__DUPLICATE_METHOD_MATCH YES)
# Set default warnings for C and C++
set(CMAKE_XCODE_ATTRIBUTE_GCC_NO_COMMON_BLOCKS YES)
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION YES)
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS NO)
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_ABOUT_MISSING_NEWLINE YES)
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_ABOUT_RETURN_TYPE YES_ERROR)
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_CHECK_SWITCH_STATEMENTS YES)
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_FOUR_CHARACTER_CONSTANTS YES)
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_SHADOW NO)
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_SIGN_COMPARE YES)
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_TYPECHECK_CALLS_TO_PRINTF YES)
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_UNDECLARED_SELECTOR YES)
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_UNINITIALIZED_AUTOS YES)
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_UNUSED_FUNCTION NO)
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_UNUSED_PARAMETER YES)
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_UNUSED_VALUE YES)
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_UNUSED_VARIABLE YES)
# Add additional warning compiler flags
set(CMAKE_XCODE_ATTRIBUTE_WARNING_CFLAGS "-Wvla -Wformat-security")
if(CMAKE_COMPILE_WARNING_AS_ERROR)
set(CMAKE_XCODE_ATTRIBUTE_GCC_TREAT_WARNINGS_AS_ERRORS YES)
endif()
# Enable color diagnostics
set(CMAKE_COLOR_DIAGNOSTICS TRUE)
# Disable usage of RPATH in build or install configurations
set(CMAKE_SKIP_RPATH TRUE)
# Have Xcode set default RPATH entries
set(CMAKE_XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../Frameworks")

View file

@ -0,0 +1,24 @@
# CMake Windows build dependencies module
include_guard(GLOBAL)
include(buildspec_common)
# _check_dependencies_windows: Set up Windows slice for _check_dependencies
function(_check_dependencies_windows)
set(arch ${CMAKE_VS_PLATFORM_NAME})
set(platform windows-${arch})
set(dependencies_dir "${CMAKE_CURRENT_SOURCE_DIR}/.deps")
set(prebuilt_filename "windows-deps-VERSION-ARCH-REVISION.zip")
set(prebuilt_destination "obs-deps-VERSION-ARCH")
set(qt6_filename "windows-deps-qt6-VERSION-ARCH-REVISION.zip")
set(qt6_destination "obs-deps-qt6-VERSION-ARCH")
set(obs-studio_filename "VERSION.zip")
set(obs-studio_destination "obs-studio-VERSION")
set(dependencies_list prebuilt qt6 obs-studio)
_check_dependencies()
endfunction()
_check_dependencies_windows()

View file

@ -0,0 +1,63 @@
# CMake Windows compiler configuration module
include_guard(GLOBAL)
include(compiler_common)
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT ProgramDatabase)
message(DEBUG "Current Windows API version: ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}")
if(CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION_MAXIMUM)
message(DEBUG "Maximum Windows API version: ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION_MAXIMUM}")
endif()
if(CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION VERSION_LESS 10.0.20348)
message(
FATAL_ERROR
"OBS requires Windows 10 SDK version 10.0.20348.0 or more recent.\n"
"Please download and install the most recent Windows platform SDK."
)
endif()
set(_obs_msvc_c_options /MP /Zc:__cplusplus /Zc:preprocessor)
set(_obs_msvc_cpp_options /MP /Zc:__cplusplus /Zc:preprocessor)
if(CMAKE_CXX_STANDARD GREATER_EQUAL 20)
list(APPEND _obs_msvc_cpp_options /Zc:char8_t-)
endif()
add_compile_options(
/W3
/utf-8
/Brepro
/permissive-
"$<$<COMPILE_LANG_AND_ID:C,MSVC>:${_obs_msvc_c_options}>"
"$<$<COMPILE_LANG_AND_ID:CXX,MSVC>:${_obs_msvc_cpp_options}>"
"$<$<COMPILE_LANG_AND_ID:C,Clang>:${_obs_clang_c_options}>"
"$<$<COMPILE_LANG_AND_ID:CXX,Clang>:${_obs_clang_cxx_options}>"
$<$<NOT:$<CONFIG:Debug>>:/Gy>
$<$<NOT:$<CONFIG:Debug>>:/GL>
$<$<NOT:$<CONFIG:Debug>>:/Oi>
)
add_compile_definitions(
UNICODE
_UNICODE
_CRT_SECURE_NO_WARNINGS
_CRT_NONSTDC_NO_WARNINGS
$<$<CONFIG:DEBUG>:DEBUG>
$<$<CONFIG:DEBUG>:_DEBUG>
)
add_link_options(
$<$<NOT:$<CONFIG:Debug>>:/OPT:REF>
$<$<NOT:$<CONFIG:Debug>>:/OPT:ICF>
$<$<NOT:$<CONFIG:Debug>>:/LTCG>
$<$<NOT:$<CONFIG:Debug>>:/INCREMENTAL:NO>
/DEBUG
/Brepro
)
if(CMAKE_COMPILE_WARNING_AS_ERROR)
add_link_options(/WX)
endif()

View file

@ -0,0 +1,18 @@
# CMake Windows defaults module
include_guard(GLOBAL)
# Enable find_package targets to become globally available targets
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL TRUE)
include(buildspec)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(
CMAKE_INSTALL_PREFIX
"$ENV{ALLUSERSPROFILE}/obs-studio/plugins"
CACHE STRING
"Default plugin installation directory"
FORCE
)
endif()

108
cmake/windows/helpers.cmake Normal file
View file

@ -0,0 +1,108 @@
# CMake Windows helper functions module
include_guard(GLOBAL)
include(helpers_common)
# set_target_properties_plugin: Set target properties for use in obs-studio
function(set_target_properties_plugin target)
set(options "")
set(oneValueArgs "")
set(multiValueArgs PROPERTIES)
cmake_parse_arguments(PARSE_ARGV 0 _STPO "${options}" "${oneValueArgs}" "${multiValueArgs}")
message(DEBUG "Setting additional properties for target ${target}...")
while(_STPO_PROPERTIES)
list(POP_FRONT _STPO_PROPERTIES key value)
set_property(TARGET ${target} PROPERTY ${key} "${value}")
endwhile()
string(TIMESTAMP CURRENT_YEAR "%Y")
set_target_properties(${target} PROPERTIES VERSION 0 SOVERSION ${PLUGIN_VERSION})
install(TARGETS ${target} RUNTIME DESTINATION "${target}/bin/64bit" LIBRARY DESTINATION "${target}/bin/64bit")
install(
FILES "$<TARGET_PDB_FILE:${target}>"
CONFIGURATIONS RelWithDebInfo Debug Release
DESTINATION "${target}/bin/64bit"
OPTIONAL
)
if(TARGET plugin-support)
target_link_libraries(${target} PRIVATE plugin-support)
endif()
add_custom_command(
TARGET ${target}
POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$<CONFIG>"
COMMAND
"${CMAKE_COMMAND}" -E copy_if_different "$<TARGET_FILE:${target}>"
"$<$<CONFIG:Debug,RelWithDebInfo,Release>:$<TARGET_PDB_FILE:${target}>>"
"${CMAKE_CURRENT_BINARY_DIR}/rundir/$<CONFIG>"
COMMENT "Copy ${target} to rundir"
VERBATIM
)
target_install_resources(${target})
get_target_property(target_sources ${target} SOURCES)
set(target_ui_files ${target_sources})
list(FILTER target_ui_files INCLUDE REGEX ".+\\.(ui|qrc)")
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "UI Files" FILES ${target_ui_files})
configure_file(cmake/windows/resources/resource.rc.in "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.rc")
target_sources(${CMAKE_PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.rc")
endfunction()
# Helper function to add resources into bundle
function(target_install_resources target)
message(DEBUG "Installing resources for target ${target}...")
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/data")
file(GLOB_RECURSE data_files "${CMAKE_CURRENT_SOURCE_DIR}/data/*")
foreach(data_file IN LISTS data_files)
cmake_path(
RELATIVE_PATH
data_file
BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/"
OUTPUT_VARIABLE relative_path
)
cmake_path(GET relative_path PARENT_PATH relative_path)
target_sources(${target} PRIVATE "${data_file}")
source_group("Resources/${relative_path}" FILES "${data_file}")
endforeach()
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" DESTINATION "${target}/data" USE_SOURCE_PERMISSIONS)
add_custom_command(
TARGET ${target}
POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$<CONFIG>/${target}"
COMMAND
"${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/data"
"${CMAKE_CURRENT_BINARY_DIR}/rundir/$<CONFIG>/${target}"
COMMENT "Copy ${target} resources to rundir"
VERBATIM
)
endif()
endfunction()
# Helper function to add a specific resource to a bundle
function(target_add_resource target resource)
message(DEBUG "Add resource '${resource}' to target ${target} at destination '${target_destination}'...")
install(FILES "${resource}" DESTINATION "${target}/data" COMPONENT Runtime)
add_custom_command(
TARGET ${target}
POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/rundir/$<CONFIG>/${target}"
COMMAND "${CMAKE_COMMAND}" -E copy "${resource}" "${CMAKE_CURRENT_BINARY_DIR}/rundir/$<CONFIG>/${target}"
COMMENT "Copy ${target} resource ${resource} to rundir"
VERBATIM
)
source_group("Resources" FILES "${resource}")
endfunction()

View file

@ -0,0 +1,105 @@
#define MyAppName "@CMAKE_PROJECT_NAME@"
#define MyAppVersion "@CMAKE_PROJECT_VERSION@"
#define MyAppPublisher "@PLUGIN_AUTHOR@"
#define MyAppURL "@PLUGIN_WEBSITE@"
[Setup]
; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{@UUID_APP@}}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={code:GetDirName}
AppendDefaultDirName=no
DefaultGroupName={#MyAppName}
OutputBaseFilename={#MyAppName}-{#MyAppVersion}-windows-installer
Compression=lzma
SolidCompression=yes
DirExistsWarning=no
AllowNoIcons=yes
; Wizard Information
WizardStyle=modern
WizardResizable=yes
SetupIconFile="../media/icon.ico"
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
Source: "..\release\Package\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "..\LICENSE"; Flags: dontcopy
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{group}\{cm:ProgramOnTheWeb,{#MyAppName}}"; Filename: "{#MyAppURL}"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
[Code]
procedure InitializeWizard();
var
GPLText: AnsiString;
Page: TOutputMsgMemoWizardPage;
begin
ExtractTemporaryFile('LICENSE');
LoadStringFromFile(ExpandConstant('{tmp}\LICENSE'), GPLText);
Page := CreateOutputMsgMemoPage(wpWelcome,
'License Information', 'Please review the license terms before installing {#MyAppName}',
'Press Page Down to see the rest of the agreement. Once you are aware of your rights, click Next to continue.',
String(GPLText)
);
end;
// credit where it's due :
// following function come from https://github.com/Xaymar/obs-studio_amf-encoder-plugin/blob/master/%23Resources/Installer.in.iss#L45
function GetDirName(Value: string): string;
var
InstallPath: string;
begin
// initialize default path, which will be returned when the following registry
// key queries fail due to missing keys or for some different reason
Result := ExpandConstant('{pf}\obs-studio');
// query the first registry value; if this succeeds, return the obtained value
if RegQueryStringValue(HKLM32, 'SOFTWARE\OBS Studio', '', InstallPath) then
Result := InstallPath;
if RegQueryStringValue(HKLM64, 'SOFTWARE\OBS Studio', '', InstallPath) then
Result := InstallPath;
end;
/////////////////////////////////////////////////////////////////////
function NextButtonClick(PageId: Integer): Boolean;
var
ObsFileName: string;
ObsMS, ObsLS: Cardinal;
ObsMajorVersion, ObsMinorVersion: Cardinal;
begin
Result := True;
if not (PageId = wpSelectDir) then begin
exit;
end;
ObsFileName := ExpandConstant('{app}\bin\64bit\obs64.exe');
if not FileExists(ObsFileName) then begin
MsgBox('OBS Studio (bin\64bit\obs64.exe) does not seem to be installed in that folder. Please select the correct folder.', mbError, MB_OK);
Result := False;
exit;
end;
Result := GetVersionNumbers(ObsFileName, ObsMS, ObsLS);
if not Result then begin
MsgBox('Failed to read version from OBS Studio (bin\64bit\obs64.exe).', mbError, MB_OK);
Result := False;
exit;
end;
{ shift 16 bits to the right to get major version }
ObsMajorVersion := ObsMS shr 16;
{ select only low 16 bits }
ObsMinorVersion := ObsMS and $FFFF;
if ObsMajorVersion < 31 then begin
MsgBox('Version of OBS Studio (bin\64bit\obs64.exe) is lower than the version 31 required.', mbError, MB_OK);
Result := False;
exit;
end;
end;

View file

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

1878
config-dialog.cpp Normal file

File diff suppressed because it is too large Load diff

121
config-dialog.hpp Normal file
View file

@ -0,0 +1,121 @@
#pragma once
#include <QCheckBox>
#include <QComboBox>
#include <QDialog>
#include <QMainWindow>
#include <qtextedit.h>
#include <obs-frontend-api.h>
#include <QGroupBox>
#include <qlistwidget.h>
#include <qspinbox.h>
#include <QFormLayout>
#include <QRadioButton>
#include "hotkey-edit.hpp"
class CanvasDock;
class OBSBasicSettings : public QDialog {
Q_OBJECT
Q_PROPERTY(QIcon generalIcon READ GetGeneralIcon WRITE SetGeneralIcon DESIGNABLE true)
Q_PROPERTY(QIcon appearanceIcon READ GetAppearanceIcon WRITE SetAppearanceIcon DESIGNABLE true)
Q_PROPERTY(QIcon streamIcon READ GetStreamIcon WRITE SetStreamIcon DESIGNABLE true)
Q_PROPERTY(QIcon outputIcon READ GetOutputIcon WRITE SetOutputIcon DESIGNABLE true)
Q_PROPERTY(QIcon audioIcon READ GetAudioIcon WRITE SetAudioIcon DESIGNABLE true)
Q_PROPERTY(QIcon videoIcon READ GetVideoIcon WRITE SetVideoIcon DESIGNABLE true)
Q_PROPERTY(QIcon hotkeysIcon READ GetHotkeysIcon WRITE SetHotkeysIcon DESIGNABLE true)
Q_PROPERTY(QIcon accessibilityIcon READ GetAccessibilityIcon WRITE SetAccessibilityIcon DESIGNABLE true)
Q_PROPERTY(QIcon advancedIcon READ GetAdvancedIcon WRITE SetAdvancedIcon DESIGNABLE true)
private:
CanvasDock *canvasDock;
QLabel *newVersion;
QListWidget *listWidget;
QComboBox *resolution;
QSpinBox *streamingVideoBitrate;
QCheckBox *streamingMatchMain;
QSpinBox *recordVideoBitrate;
QCheckBox *recordingMatchMain;
QComboBox *audioBitrate;
QComboBox *virtualCameraMode;
QCheckBox *backtrackClip;
QSpinBox *backtrackDuration;
QLineEdit *backtrackPath;
QCheckBox *maxSizeEnable;
QSpinBox *maxSize;
QCheckBox *maxTimeEnable;
QSpinBox *maxTime;
QLabel *multitrackLabel;
QFormLayout *streamingLayout;
std::vector<QLineEdit *> server_names;
std::vector<QComboBox *> servers;
std::vector<QLineEdit *> keys;
std::vector<QCheckBox *> servers_enabled;
QCheckBox *streamDelayEnable;
QSpinBox *streamDelayDuration;
QCheckBox *streamDelayPreserve;
QCheckBox *streamingUseMain;
std::vector<QRadioButton *> streamingAudioTracks;
QComboBox *streamingEncoder;
obs_properties_t *stream_encoder_properties = nullptr;
obs_data_t *stream_encoder_settings = nullptr;
std::map<obs_property_t *, QWidget *> stream_encoder_property_widgets;
QLineEdit *recordPath;
QLineEdit *recordFileFormat;
QCheckBox *recordingUseMain;
QLineEdit *filenameFormat;
QComboBox *fileFormat;
std::vector<QCheckBox *> recordingAudioTracks;
QComboBox *recordingEncoder;
obs_properties_t *record_encoder_properties = nullptr;
obs_data_t *record_encoder_settings = nullptr;
std::map<obs_property_t *, QWidget *> record_encoder_property_widgets;
std::vector<OBSHotkeyWidget *> hotkeys;
QIcon GetGeneralIcon() const;
QIcon GetAppearanceIcon() const;
QIcon GetStreamIcon() const;
QIcon GetOutputIcon() const;
QIcon GetAudioIcon() const;
QIcon GetVideoIcon() const;
QIcon GetHotkeysIcon() const;
QIcon GetAccessibilityIcon() const;
QIcon GetAdvancedIcon() const;
std::vector<obs_hotkey_t *> GetHotKeysFromOutput(obs_output_t *obs_output);
std::vector<obs_key_combination_t> GetCombosForHotkey(obs_hotkey_id hotkey);
std::vector<obs_hotkey_t *> GetHotkeyById(obs_hotkey_id hotkey);
obs_hotkey_t *GetHotkeyByName(QString name);
void SetEncoderBitrate(obs_encoder_t *obs_encoder, bool record);
void AddProperty(obs_property_t *property, obs_data_t *settings, QFormLayout *layout,
std::map<obs_property_t *, QWidget *> *widgets);
void LoadProperty(obs_property_t *property, obs_data_t *settings, QWidget *widget);
void RefreshProperties(std::map<obs_property_t *, QWidget *> *widgets, QFormLayout *layout);
void AddServer();
private slots:
void SetGeneralIcon(const QIcon &icon);
void SetAppearanceIcon(const QIcon &icon);
void SetStreamIcon(const QIcon &icon);
void SetOutputIcon(const QIcon &icon);
void SetAudioIcon(const QIcon &icon);
void SetVideoIcon(const QIcon &icon);
void SetHotkeysIcon(const QIcon &icon);
void SetAccessibilityIcon(const QIcon &icon);
void SetAdvancedIcon(const QIcon &icon);
public:
OBSBasicSettings(CanvasDock *canvas_dock, QMainWindow *parent = nullptr);
~OBSBasicSettings();
void LoadSettings();
void SaveSettings();
public slots:
};

BIN
data/images/overflow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

72
data/locale/bn-BD.ini Normal file
View file

@ -0,0 +1,72 @@
Vertical="Vertical"
VerticalCanvas="Vertical Canvas"
Description="Vertical Canvas"
Canvas="ক্যানভাস"
VirtualCam="ভার্চুয়াল ক্যামেরা"
LinkedScenes="লিঙ্ক করা Scenes"
OnMainCanvas="প্রধান ক্যানভাসে দেখাও"
SceneName="Scene নাম"
SourceName="Source নাম"
TransitionName="Transition নাম"
Saving="Backtrack সংরক্ষণ করা হচ্ছে..."
Saved="Backtrack সংরক্ষিত!"
VerticalSettings="Vertical সেটিংস"
General="General"
Resolution="Resolution"
ShowScenes="মূল scene list এ vertical scenes দেখাও"
Backtrack="Backtrack"
BacktrackEnable="Backtrack স্ট্রিমিং/রেকর্ডিংয়ের সময় চলে"
BacktrackOn="Backtrack চালু"
BacktrackOff="Backtrack বন্ধ"
BacktrackDuration="Backtrack রেকর্ডিং দৈর্ঘ্য"
BacktrackPath="Backtrack রেকর্ডিং পথ"
StartBacktrackHotkey="Backtrack Hotkey শুরু করুন"
StopBacktrackHotkey="Backtrack Hotkey বন্ধ করুন"
SaveBacktrackHotkey="Backtrack Hotkey সংরক্ষণ করুন"
Streaming="স্ট্রিমিং"
ViewGuide="গাইড দেখুন"
Name="নাম"
Server="সার্ভার"
Key="কী (Key)"
Enabled="Enabled"
Output="আউটপুট"
StartStreamingHotkey="স্ট্রিমিং Hotkey শুরু করুন"
StopStreamingHotkey="স্ট্রিমিং Hotkey বন্ধ করুন"
Recording="রেকর্ডিং"
StartRecordingHotkey="রেকর্ডিং Hotkey শুরু করুন"
StopRecordingHotkey="রেকর্ডিং Hotkey বন্ধ করুন"
Version="ভার্সন"
MadeBy="♡ দ্বারা তৈরি"
AddVerticalScene="vertical scene যুক্ত করুন"
RemoveVerticalScene="vertical scene অপসারণ করুন"
StreamVertical="সব vertical আউটপুটগুলো stream করুন"
StreamVerticalMulti="পৃথক vertical আউটপুটগুলো নিয়ন্ত্রন করুন"
RecordVertical="vertical রেকর্ড করুন"
BacktrackClipVertical="Backtrack clip vertical"
VirtualCameraVertical="Vertical ভার্চুয়াল ক্যামেরা"
VerticalScene="Vertical Scene"
tiktokError="আমরা লক্ষ্য করেছি যে আপনি টিকটকে স্ট্রিম করার চেষ্টা করছেন। দয়া করে নিশ্চিত করুন যে আপনার স্ট্রিম কি আপডেট করা আছে, কারণ টিকটক প্রতিটি স্ট্রিমের পরে এগুলো পরিবর্তন করে!"
backtrackStartFail="Backtrack শুরু করা ব্যর্থ হয়েছে"
backtrackCustomFfmpeg="backtrack শুরু করা যাচ্ছে না যখন রেকর্ডিং custom ffmpeg এ সেট করা আছে।"
backtrackNoReplayBuffer="backtrack শুরু করা যাচ্ছে না কারণ কোনো রিপ্লে বাফার পাওয়া যায়নি।"
UseMain="main OBS সেটিংস ব্যবহার করুন"
NewVersion="নতুন ভার্সন (%1) <a href='https://aitum.tv/download/vertical/'>এখানে/a> পাওয়া যাবে"
RecordPathError="কনফিগার করা Recording Path খালি রয়েছে। আপনার Recording Path টি Vertical Settings → Recording এর অধীনে পরীক্ষা করুন।"
Help="সাহায্য"
HelpSupport="Aitum Discord সার্ভারে যোগ দিন"
CopyFromMain="main থেকে copy করুন"
VirtualCameraWarning="vertical ভার্চুয়াল ক্যামেরা চালু করার পর, OBS কিছু সেটিংস edit করতে বাধা দিতে পারে কারণ এটি মনে করতে পারে ক্যামেরাটি এখনও সক্রিয়, এমনকি এটি বন্ধ হওয়ার পরেও। সেটিংস আবার edit করতে OBS রিস্টার্ট করুন।"
NoOutputServer="কোনো আউটপুট সার্ভার নেই"
NoOutputServerWarning="কোনো সক্রিয় আউটপুট সার্ভার পাওয়া যায়নি। নিশ্চিত করুন যে vertical streaming সেটিংস এ একটি আউটপুট সক্রিয় করা আছে।"
StartAll="সব শুরু করুন"
StopAll="সব বন্ধ করুন"
HelpIntro="সমস্যা সম্মুখীন হচ্ছেন? Vertical সম্পর্কে আরও জানতে চান? নীচে আমাদের সহায়ক রিসোর্সগুলি দেখুন।"
HelpTroubleshooterButton="Vertical ট্রাবলশুটার ওয়েবসাইটে ভিজিট করুন"
HelpGuideButton="Vertical গাইডস ওয়েবসাইটে ভিজিট করুন"
HelpDiscordButton="Aitum Discord ভিজিট করুন"
VirtualCameraModeVertical="Vertical"
VirtualCameraModeMain="Main"
VirtualCameraModeBoth="Both"
StreamingMatchMain="main OBS স্ট্রিম শুরু ও শেষ হলে Vertical স্ট্রিমিংও শুরু এবং শেষ হবে"
RecordingMatchMain="main OBS স্ট্রিম শুরু ও শেষ হলে Vertical স্ট্রিমিংও শুরু এবং শেষ হবে"
OutputsMultistream="যেহেতু আপনি Aitum Multistream ব্যবহার করছেন, আপনাকে Vertical স্ট্রিমিং আউটপুটগুলি Aitum Multistream সেটিংস থেকে নিয়ন্ত্রণ করতে হবে।\nএখানে আউটপুট সেটিংস পরিবর্তন করা নিষ্ক্রিয় করা হয়েছে।"

67
data/locale/de-DE.ini Normal file
View file

@ -0,0 +1,67 @@
Vertical="Vertikal"
VerticalCanvas="Vertikale Leinwand"
Description="Vertikale Leinwand"
Canvas="Leinwand"
VirtualCam="Virtuelle Kamera"
LinkedScenes="Verlinkte Szenen"
OnMainCanvas="Zeige auf Hauptleinwand"
SceneName="Szenen Name"
SourceName="Quellen Name"
TransitionName="Übergang Name"
Saving="Backtrack speichern..."
Saved="Backtrack gespeichert!"
VerticalSettings="Vertikale Einstellungen"
General="Allgemein"
Resolution="Auflösung"
ShowScenes="Zeige vertikale Szenen in Hauptszenen Liste"
Backtrack="Backtrack"
BacktrackEnable="Backtrack läuft während des Streamings/Aufnahme"
BacktrackAlwaysOn="Backtrack immer an"
BacktrackDuration="Backtrack Aufnahmelänge"
BacktrackPath="Backtrack Aufnahme Pfad"
SaveBacktrackHotkey="Speichere Backtrack Hotkey"
Streaming="Streaming"
ViewGuide="Ratgeber anschauen"
Name="Name"
Server="Server"
Key="Schlüssel"
Enabled="Aktiviert"
Output="Ausgabe"
StartStreamingHotkey="Start Streaming Hotkey"
StopStreamingHotkey="Stoppe Streaming Hotkey"
Recording="Aufnahme"
StartRecordingHotkey="Start Aufnahme Hotkey"
StopRecordingHotkey="Stoppe Aufnahme Hotkey"
Version="Version"
MadeBy="made with ♡ by"
AddVerticalScene="Füge vertikale Szene hinzu"
RemoveVerticalScene="Entferne vertikale Szene"
StreamVertical="Streame alle vertikalen Ausgaben"
RecordVertical="Aufnahme Vertikal"
BacktrackClipVertical="Backtrack Clip vertikal"
VirtualCameraVertical="Virtuelle Kamera vertikal"
VerticalScene="Vertikale Szene"
tiktokError="Hinweis, wir haben festgestellt das Du versuchst auf TikTok zu streamen, stelle sicher das Dein StreamKey aktualisiert ist, da TikTok diesen nach jedem Stream rotiert!"
backtrackStartFail="Backtrack starting fehlgeschlagen"
backtrackCustomFfmpeg="Backtrack kann nicht gestartet werden, wenn custom ffmpeg Einstellungen vorhanden sind"
backtrackNoReplayBuffer="Backtrack kann nicht gestartet werden, wenn kein Wiederholungsbuffer gefunden wurde"
UseMain="Benutze OBS Haupteinstellungen"
NewVersion="Neue Version (%1) verfügbar <a href='https://aitum.tv/download/vertical/'>hier</a>"
RecordPathError="Der eingestellte Aufnahmepfad ist leer. Prüfe den Aufnahmepfad unter Vertikale Einstellungen → Aufnahme."
Help="Hilfe"
HelpSupport="Trete dem Aitum Discord Server bei"
CopyFromMain="Kopiere von Main"
VirtualCameraWarning="Nach dem starten der vertikalen Kamera ist es evtl. nicht möglich OBS Einstellungen zu ändern, weil OBS denkt es wäre noch aktiv nachdem es gestoppt wurde. OBS muss neugestartet werden um Einstellungen zu ändern."
NoOutputServer="Kein Ausgabe Server"
NoOutputServerWarning="Es wurde kein Ausgabe Server gefunden. Stelle sicher das eine Ausgabe in den vertikalen Streaming Einstellungen ausgewählt ist."
StartAll="Starte alle"
StopAll="Stoppe alle"
HelpIntro="Ein Problem aufgetreten? Du möchtest mehr über Vertikal erfahren? Prüfe unsere Hilfsresourcen unten."
HelpTroubleshooterButton="Besuche die Vertikal Ratgeber Webseite"
HelpGuideButton="Besuche die Vertikal Hilfe Webseite"
HelpDiscordButton="Besuche den Aitum Discord"
VirtualCameraModeVertical="Vertikal"
VirtualCameraModeMain="Haupt"
VirtualCameraModeBoth="Beide"
StreamingMatchMain="Starte und stoppe das Streaming wenn Haupt OBS Streaming startet oder stoppt"
RecordingMatchMain="Starte und stoppe die Aufnahme wenn Haupt OBS Aufnahme startet oder stoppt"

81
data/locale/en-US.ini Normal file
View file

@ -0,0 +1,81 @@
Vertical="Vertical"
VerticalCanvas="Vertical Canvas"
Description="Vertical Canvas"
Canvas="Canvas"
VirtualCam="Virtual camera"
LinkedScenes="Linked Scenes"
OnMainCanvas="Show on main canvas"
SceneName="Scene Name"
SourceName="Source Name"
TransitionName="Transition Name"
Saving="Backtrack saving..."
Saved="Backtrack saved!"
VerticalSettings="Vertical Settings"
General="General"
Resolution="Resolution"
ShowScenes="Show vertical scenes in main scene list"
Backtrack="Backtrack"
BacktrackEnable="Backtrack runs while streaming/recording"
BacktrackOn="Backtrack on"
BacktrackOff="Backtrack off"
BacktrackDuration="Backtrack Recording Length"
BacktrackPath="Backtrack Recording Path"
StartBacktrackHotkey="Start Backtrack Hotkey"
StopBacktrackHotkey="Stop Backtrack Hotkey"
SaveBacktrackHotkey="Save Backtrack Hotkey"
Streaming="Streaming"
ViewGuide="View Guide"
Name="Name"
Server="Server"
Key="Key"
Enabled="Enabled"
Output="Output"
StartStreamingHotkey="Start Streaming Hotkey"
StopStreamingHotkey="Stop Streaming Hotkey"
Recording="Recording"
StartRecordingHotkey="Start Recording Hotkey"
StopRecordingHotkey="Stop Recording Hotkey"
Version="Version"
MadeBy="made with ♡ by"
AddVerticalScene="Add vertical scene"
RemoveVerticalScene="Remove vertical scene"
StreamVertical="Stream all vertical outputs"
StreamVerticalMulti="Control individual vertical outputs"
RecordVertical="Record vertical"
BacktrackClipVertical="Backtrack clip vertical"
VirtualCameraVertical="Vertical virtual camera"
VerticalScene="Vertical Scene"
tiktokError="Additionally, as we've detected you are trying to stream to TikTok, please ensure your stream key is updated as TikTok cycles them after every stream!"
backtrackStartFail="Backtrack starting failed"
backtrackCustomFfmpeg="Can't start backtrack when recording is set to custom ffmpeg"
backtrackNoReplayBuffer="Can't start backtrack when no replay buffer is found"
UseMain="Use main OBS settings"
NewVersion="New version (%1) available <a href='https://aitum.tv/download/vertical/'>here</a>"
RecordPathError="The configured Recording Path is empty. Please check your Recording Path under Vertical Settings → Recording."
Help="Help"
HelpSupport="Join the Aitum Discord Server"
CopyFromMain="Copy from main"
VirtualCameraWarning="After starting the vertical virtual camera you might not be able to edit some OBS settings because it thinks it is still active after it stopped. Restart OBS to be able edit the settings again."
NoOutputServer="No output server"
NoOutputServerWarning="There is no enabled output server found. Make sure you have a output enabled in the vertical streaming settings."
StartAll="Start all"
StopAll="Stop all"
HelpIntro="Encountering an issue? Want to learn more about Vertical? Check out our help resources below."
HelpTroubleshooterButton="Visit the Vertical Troubleshooter website"
HelpGuideButton="Visit the Vertical Guides website"
HelpDiscordButton="Visit the Aitum Discord"
VirtualCameraModeVertical="Vertical"
VirtualCameraModeMain="Main"
VirtualCameraModeBoth="Both"
StreamingMatchMain="Start and stop streaming when main OBS starts and stops streaming"
RecordingMatchMain="Start and stop recording when main OBS starts and stops recording"
OutputsMultistream="As you are using Aitum Multistream, you must control Vertical streaming outputs from the Aitum Multistream Settings.\nChanging Output settings is disabled here."
VerticalDonate="Support Aitum"
SupportButton="Support Aitum"
SupportTitle="Supporting Aitum"
SupportText="Aitum Vertical is provided free by the Aitum Team for <u>you, the streaming community</u>.<br /><br />Your support is essential in helping us to continue our journey of making useful tools that empower creators to make awesome content.<br /><br />If our tools have helped streamline your workflows, enabled your channel to grow or even inspired you to start crearting content, please consider supporting our team.<br /><br /><strong><a href='https://aitum.tv/support-us'>Support the Aitum Team</a></strong>"
BacktrackEncoder="Backtrack quality is set in the recording settings"
Multitrack="Multitrack video"
MultitrackDisabled="In the OBS stream settings you have selected a service that does not support Multitrack video or you have Multitrack video disabled"
MultitrackVerticalSelected="Aitum Vertical is selected as extra canvas for Multitrack video streaming"
MultitrackVerticalNotSelected="In the OBS stream settings you have Multitrack video enabled, but you do not have Aitum Vertical selected as aditional canvas"

77
data/locale/es-ES.ini Normal file
View file

@ -0,0 +1,77 @@
Vertical="Vertical"
VerticalCanvas="Lienzo vertical"
Description="Lienzo vertical"
Canvas="Lienzo"
VirtualCam="Cámara virtual"
LinkedScenes="Escenas enlazadas"
OnMainCanvas="Mostrar en el lienzo principal"
SceneName="Nombre de la escena"
SourceName="Nombre de la fuente"
TransitionName="Nombre de la transición"
Saving="Guardando backtrack..."
Saved="¡Backtrack guardado!"
VerticalSettings="Ajustes verticales"
General="General"
Resolution="Resolución"
ShowScenes="Mostrar escenas verticales en la lista de escenas principal"
Backtrack="Backtrack"
BacktrackEnable="Backtrack se ejecuta durante la transmisión/grabación"
BacktrackOn="Backtrack activado"
BacktrackOff="Backtrack desactivado"
BacktrackDuration="Duración de la grabación"
BacktrackPath="Ruta de la grabación"
StartBacktrackHotkey="Hotkey para iniciar Backtrack"
StopBacktrackHotkey="Hotkey para detener Backtrack"
SaveBacktrackHotkey="Hotkey de guardado"
Streaming="Emisión"
ViewGuide="Ver guía"
Name="Nombre"
Server="Servidor"
Key="Clave"
Enabled="Habilitada"
Output="Salida"
StartStreamingHotkey="Hotkey para iniciar emisión"
StopStreamingHotkey="Hotkey para detener emisión"
Recording="Grabación"
StartRecordingHotkey="Hotkey para iniciar transmisión"
StopRecordingHotkey="Hotkey para detener transmisión"
Version="Versión"
MadeBy="hecha con ♡ por"
AddVerticalScene="Agregar escena vertical"
RemoveVerticalScene="Eliminar escena vertical"
StreamVertical="Transmisión vertical"
StreamVerticalMulti="Control de salidas verticales individuales"
RecordVertical="Grabación vertical"
BacktrackClipVertical="Backtrack clip vertical"
VirtualCameraVertical="Cámara virtual vertical"
VerticalScene="Escena vertical"
tiktokError="Además, como hemos detectado que estás intentando transmitir a TikTok, asegúrate de que tu clave de transmisión esté actualizada ya que TikTok la cambia después de cada transmisión."
backtrackStartFail="Error al iniciar el backtrack"
backtrackCustomFfmpeg="No se puede iniciar el backtrack cuando la grabación está configurada como ffmpeg personalizado"
backtrackNoReplayBuffer="No se puede iniciar un backtrack cuando no está activo el búfer de repetición"
UseMain="Usar la configuración principal de OBS"
NewVersion="Nueva version (%1) disponible <a href='https://aitum.tv/download/vertical/'>aquí</a>"
RecordPathError="La ruta de grabación configurada está vacía. Verifica tu ruta de grabación en Ajustes verticales → Grabación."
Help="Ayuda"
HelpSupport="Únete al servidor de Discord de Aitum"
CopyFromMain="Copiar de las transiciones principales"
VirtualCameraWarning="Después de iniciar la cámara virtual vertical es posible que no puedas editar algunos ajustes de OBS porque él cree que todavía está activa después de detenerse. Reinicia OBS para poder editar los ajustes de nuevo."
NoOutputServer="No hay servidor de salida"
NoOutputServerWarning="No se ha encontrado ningún servidor de salida habilitado. Asegúrese de que tiene una salida habilitada en la configuración de transmisión vertical."
StartAll="Iniciar todas"
StopAll="Detener todas"
HelpIntro="¿Encontraste algún problema? ¿Quieres saber más sobre Vertical? Consulta nuestros artículos de ayuda."
HelpTroubleshooterButton="Visita la página de resolución de problemas de Vertical"
HelpGuideButton="Visita la página de guías de Vertical"
HelpDiscordButton="Visita el Discord de Aitum"
VirtualCameraModeVertical="Vertical"
VirtualCameraModeMain="Principal"
VirtualCameraModeBoth="Ambas"
StreamingMatchMain="Iniciar y detener la transmisión cuando OBS inicia y detiene la transmisión"
RecordingMatchMain="Iniciar y detener la grabación cuando OBS inicia y detiene la grabación"
OutputsMultistream="Al estar usando Aitum Multistream, debes controlar las salidas de transmisión verticales desde los ajustes de Aitum Multistream.\nCambiar las opciones de salida está deshabilitado aquí."
VerticalDonate="Apoya a Aitum"
SupportButton="Apoya a Aitum"
SupportTitle="Apoyando a Aitum"
SupportText="Aitum Vertical se proporciona de forma gratuita por el equipo de Aitum para <u>vosotros, la comunidad de streaming</u>.<br /><br />Vuestro apoyo es esencial para ayudarnos a continuar nuestro viaje de crear herramientas útiles que potencien a los creadores a realizar contenidos increíbles.<br /><br />Si nuestras herramientas te han ayudado a optimizar tu flujo de trabajo, han permitido que tu canal crezca o incluso te han inspirado a comenzar a crear contenido, considera apoyar a nuestro equipo.<br /><br /><strong><a href='https://aitum.tv/support-us'>Apoya al equipo de Aitum</a></strong>"
BacktrackEncoder="La calidad del Backtrack se establece en los ajustes de grabación"

66
data/locale/nl-NL.ini Normal file
View file

@ -0,0 +1,66 @@
Vertical="Verticaal"
VerticalCanvas="Verticale Canvas"
Description="Verticale Canvas"
Canvas="Canvas"
VirtualCam="Virtuele camera"
LinkedScenes="Gelinkte Scènes"
OnMainCanvas="Laat zien op het hoofd canvas"
SceneName="Scène Naam"
SourceName="Bron Naam"
TransitionName="Overgang Naam"
Saving="Backtrack aan het opslaan..."
Saved="Backtrack opgeslagen!"
VerticalSettings="Verticale Instellingen"
General="Algemeen"
Resolution="Resolutie"
ShowScenes="Laat verticale scènes zien in de hoofd scène lijst"
Backtrack="Backtrack"
BacktrackEnable="Backtrack staat aan tijdends het streamen en/of opnemen"
BacktrackAlwaysOn="Backtrack staat altijd aan"
BacktrackDuration="Backtrack Opname Lengte"
BacktrackPath="Backtrack Opnamepad"
SaveBacktrackHotkey="Backtrack Opslaan Sneltoets"
Streaming="Streamen"
ViewGuide="Bekijk Gids"
Name="Naam"
Server="Server"
Key="Stream key"
Enabled="Ingeschakeld"
Output="Uitvoer"
StartStreamingHotkey="Streamen Starten Sneltoets"
StopStreamingHotkey="Streamen Stoppen Sneltoets"
Recording="Opnemen"
StartRecordingHotkey="Opname Starten Sneltoets"
StopRecordingHotkey="Opname Stoppen Sneltoets"
Version="Versie"
MadeBy="gemaakt met ♡ door"
AddVerticalScene="Voeg verticale scène toe"
RemoveVerticalScene="Verwijder verticale scène"
StreamVertical="Alle verticale stream uitvoeren starten"
StreamVerticalMulti="Afzonderlijke verticale uitvoeren bedienen"
RecordVertical="Verticaal opnemen starten"
BacktrackClipVertical="Backtrack clip verticaal"
VirtualCameraVertical="Verticale virtuele camera"
VerticalScene="Verticale Scène"
tiktokError="Verder hebben we gedetecteerd dat je naar TikTok probeert te streamen. Je moet ervoor zorgen dat je stream key is bijgewerkt, aangezien TikTok ze na elke stream verandered!"
backtrackStartFail="Starten van Backtrack is gefaald"
backtrackCustomFfmpeg="Backtrack kan niet gestart worden wanneer opname is ingesteld als 'Aangepaste Uitvoer (FFmpeg)'"
backtrackNoReplayBuffer="Kan backtrack niet starten wanneer er geen replay buffer gevonden is"
UseMain="Gebruik de hoofdinstellingen van OBS"
NewVersion="Er is <a href='https://aitum.tv/download/vertical/'>hier</a> een nieuwe versie (%1) beschikbaar"
RecordPathError="Het geconfigureerde bestandspad is leeg. Controleer alstublieft het bestandspad onder Verticale Instellingen → Opnemen"
Help="Help"
HelpSupport="Word lid van de Aitum Discord server"
CopyFromMain="Kopieer van de de hoofd scène lijst"
VirtualCameraWarning="Na het starten van de verticale virtuele camera is het mogelijk dat je sommige OBS instellingen niet meer kan aanpassen. Dit is omdat OBS denkt dat de virtuele camera nogsteeds actief is. Herstart OBS om alle instellingen weer aan te kunnen passen"
NoOutputServer="Geen uitvoerserver"
NoOutputServerWarning="Er is geen ingeschakelde uitvoerserver gevonden. Zorg ervoor dat u een uitvoer hebt ingeschakeld in de verticale streaming-instellingen."
StartAll="Alles starten"
StopAll="Alles stoppen"
HelpIntro="Heeft u een probleem ondervonden? Wilt u meer weten over Verticaal? Bekijk onze hulpmiddelen hieronder."
HelpTroubleshooterButton="Bezoek de probleemoplosser website voor Verticaal"
HelpGuideButton="Bezoek de gids website voor Verticaal"
HelpDiscordButton="Bezoek de Aitum Discord"
VirtualCameraModeVertical="Verticaal"
VirtualCameraModeMain="Hoofd"
VirtualCameraModeBoth="Beide"

49
data/locale/pl-PL.ini Normal file
View file

@ -0,0 +1,49 @@
Vertical="Pionowy"
VerticalCanvas="Pionowy obszar roboczy"
Description="Pionowy obszar roboczy"
Canvas="Obszar roboczy"
VirtualCam="Wirtualna kamera"
LinkedScenes="Powiązane sceny"
OnMainCanvas="Pokaż w głównym obszarze roboczym"
SceneName="Nazwa sceny"
SourceName="Nazwa źródła"
TransitionName="Nazwa przejścia"
Saving="Zapisuję Backtrack..."
Saved="Backtrack zapisany!"
VerticalSettings="Ustawienia pionu"
General="Glówne"
Resolution="Rozdzielczość"
ShowScenes="Pokaż pionowe sceny na głównej liście scen"
Backtrack="Backtrack"
BacktrackEnable="Backtrack działa kiedy streamujesz/nagrywasz"
BacktrackAlwaysOn="Backtrack zawsze włączony"
BacktrackDuration="Długość nagrania Backtrack"
BacktrackPath="Miejsce zapisu pliku Backtrack"
SaveBacktrackHotkey="Skrót: zapisz Backtrack"
Streaming="Streaming"
Server="Serwer"
Key="Klucz"
StartStreamingHotkey="Skrót: rozpocznij stream"
StopStreamingHotkey="Skrót: zatrzymaj stream"
Recording="Nagrywanie"
StartRecordingHotkey="Skrót: rozpocznij nagrywanie"
StopRecordingHotkey="Skrót: zatrzymaj nagrywanie"
Version="Wersja"
MadeBy="stworzone z ♡ przez"
AddVerticalScene="Dodaj pionową scenę"
RemoveVerticalScene="Usuń pionową scenę"
StreamVertical="Pionowy stream"
RecordVertical="Pionowe nagranie"
BacktrackClipVertical="Pionowy klip Backtrack"
VirtualCameraVertical="Pionowa wirtualna kamera"
VerticalScene="Pionowa scena"
tiktokError="Additionally, as we've detected you are trying to stream to TikTok, please ensure your stream key is updated as TikTok cycles them after every stream!"
backtrackStartFail="Rozpoczęcie Backtrack nieudane"
backtrackCustomFfmpeg="Nie możesz włączyć Backtrack jeżeli nagrywanie jest ustawione na custom ffmpeg"
backtrackNoReplayBuffer="Nie możesz włączyć Backtrack jeżeli nie znaleziono nagrywania powtórek."
UseMain="Użyj ustawień OBS"
NewVersion="Nowa wersja (%1) dostępna <a href='https://aitum.tv/download/vertical/'>tutaj</a>"
RecordPathError="Skonfigurowana ścieżka dostępu do nagrań jest pusta. Sprawdź proszę swoją ścieżkę dostępu w Pionowe ustawienia → Nagrywanie."
Help="Pomoc"
HelpSupport="Dołącz do naszego serwera Discord: Aitum"
CopyFromMain="Skopiuj z głównej"

68
data/locale/pt-BR.ini Normal file
View file

@ -0,0 +1,68 @@
Vertical="Vertical"
VerticalCanvas="Projetor Vertical"
Description="Projetor Vertical"
Canvas="Projetor"
VirtualCam="Câmera Virtual"
LinkedScenes="Cenas Vinculadas"
OnMainCanvas="Mostrar na Projetor principal"
SceneName="Nome da Cena"
SourceName="Nome da Fonte"
TransitionName="Nome da Transição"
Saving="Salvando Backtrack..."
Saved="Backtrack salvo!"
VerticalSettings="Configurações Verticais"
General="Geral"
Resolution="Resolução"
ShowScenes="Mostrar cenas verticais na lista principal de cenas"
Backtrack="Backtrack"
BacktrackEnable="O Backtrack fica ativo enquanto você transmite/grava"
BacktrackAlwaysOn="Backtrack sempre ativo"
BacktrackDuration="Tamanho da Gravação do Backtrack"
BacktrackPath="Local de Salvamento do Backtrack"
SaveBacktrackHotkey="Botão de Salvamento do Backtrack"
Streaming="Transmitindo"
ViewGuide="Ver Guia"
Name="Nome"
Server="Servidor"
Key="Chave"
Enabled="Ativo"
Output="Saída"
StartStreamingHotkey="Botão de Início da Transmissão"
StopStreamingHotkey="Botão de Término da Transmissão"
Recording="Gravando"
StartRecordingHotkey="Botão de Início da Gravação"
StopRecordingHotkey="Botão de Término da Gravação"
Version="Versão"
MadeBy="feito com ♡ por"
AddVerticalScene="Adicionar cena vertical"
RemoveVerticalScene="Remover cena vertical"
StreamVertical="Transmitir todas as saídas verticais"
StreamVerticalMulti="Controlar saídas verticais individuais"
RecordVertical="Gravação Vertical"
BacktrackClipVertical="Criar clipe vertical"
VirtualCameraVertical="Câmera virtual vertical"
VerticalScene="Cena Vertical"
tiktokError="Junto a isso, já que detectamos que você está tentando transmitir para o TikTok, por favor verifique se sua chanve de transmissão foi atualizada, já que o TikTok as muda após cada transmissão!"
backtrackStartFail="Falha ao iniciar o Backtrack"
backtrackCustomFfmpeg="Impossível iniciar o backtrack quando a gravação está configurada como custom ffmpeg"
backtrackNoReplayBuffer="Impossível iniciar o backtrack quando nenhum replay buffer é encontrado"
UseMain="Usar configurações principais do OBS"
NewVersion="Nova versão (%1) disponível <a href='https://aitum.tv/download/vertical/'>aqui</a>"
RecordPathError="O Caminho do Local de Salvamento está vazio. Por favor verifique seu Caminho do Local de Salvamento em Configurações Verticais → Gravação."
Help="Ajuda"
HelpSupport="Entrar no Servidor de Discord do Aitum"
CopyFromMain="Copiar do principal"
VirtualCameraWarning="Após iniciar a câmera virtual vertical, pode ser que você não consiga editar algumas configurações do OBS, pois ele vai achar que ela ainda está ativa após ter paradoo. Reinicie o OBS para conseguir editar as configurações novamente."
NoOutputServer="Nenhum servidor de saída"
NoOutputServerWarning="Nenhum servidor de saída ativo foi encontrado. Certifique-se que você t possui uma saída ativa nas configurações de transmissão do Vertical."
StartAll="Iniciar todos"
StopAll="Parar todos"
HelpIntro="Encontrou um problema? Quer aprender mais sobre o Vertical? Veja os nossos recursos de ajuda abaixo."
HelpTroubleshooterButton="Visitar o website de Soluções de Problemas do Vertical"
HelpGuideButton="Visitar o webiste de Regras do Vertical"
HelpDiscordButton="Visitar o Discord do Aitum"
VirtualCameraModeVertical="Vertical"
VirtualCameraModeMain="Principal"
VirtualCameraModeBoth="Ambos"
StreamingMatchMain="Começar e parar a transmissão quando o OBS principal começar e parar de transmitir"
RecordingMatchMain="Começar e parar de gravar quando o OBS principal começar e parar de gravar"

68
data/locale/ru-RU.ini Normal file
View file

@ -0,0 +1,68 @@
Vertical="Vertical"
VerticalCanvas="Vertical Canvas"
Description="Vertical Canvas"
Canvas="Холст"
VirtualCam="Виртуальная камера"
LinkedScenes="Связанные сцены"
OnMainCanvas="Показать на главном холсте"
SceneName="Название сцены"
SourceName="Название источника"
TransitionName="Название перехода"
Saving="Backtrack сохранение..."
Saved="Backtrack сохранен!"
VerticalSettings="Vertical Настройки"
General="Основные"
Resolution="Разрешение"
ShowScenes="Показать вертикальные сцены в основном списке"
Backtrack="Backtrack"
BacktrackEnable="Backtrack будет включен во время трансляции/записи"
BacktrackOn="Backtrack включен"
BacktrackOff="Backtrack выключен"
BacktrackDuration="Backtrack продолжительность записи"
BacktrackPath="Backtrack путь до записи"
StartBacktrackHotkey="Горячая клавиша запуска Backtrack"
StopBacktrackHotkey="Горячая клавиша остановки Backtrack"
SaveBacktrackHotkey="Горячая клавиша сохранения Backtrack"
Streaming="Трансляция"
ViewGuide="Посмотреть руководство"
Name="Имя"
Server="Сервер"
Key="Ключ"
Enabled="Включено"
Output="Вывод"
StartStreamingHotkey="Горячая клавиша запуска трансляции"
StopStreamingHotkey="Горячая клавиша остановки трансляции"
Recording="Запись"
StartRecordingHotkey="Горячая клавиша начала записи"
StopRecordingHotkey="Горячая клавиша остановки записи"
Version="Версия"
MadeBy="сделано с ♡ в "
AddVerticalScene="Добавить вертикальную сцену"
RemoveVerticalScene="Удалить вертикальную сцену"
StreamVertical="Транслировать все вертикальные выводы"
StreamVerticalMulti="Управление отдельными вертикальными выводами"
RecordVertical="Vertical Запись"
BacktrackClipVertical="Backtrack клип"
VirtualCameraVertical="Vertical виртуальная камера"
VerticalScene="Vertical Сцена"
tiktokError="Пожалуйста, убедитесь, что ключ странсляции обновлен! TikTok обновляет ключ для каждой новой трансляции!"
backtrackStartFail="Backtrack ошибка запуска"
backtrackCustomFfmpeg="Невозможно запустить Backtrack пока запись настроена с ffmpeg"
backtrackNoReplayBuffer="Нельзя запусть Backtrack если нет буфера повтора"
UseMain="Использовать основные настройки OBS"
NewVersion="Доступна <a href='https://aitum.tv/download/vertical/'>новая версия</a> (%1)"
RecordPathError="Указанный путь для записи пуст. Пожалуйста, проверьте путь в разделе Vertical Настройки → Запись."
Help="Помощь"
HelpSupport="Подключиться к Discord серверу Aitum"
CopyFromMain="Скопировать из основного"
VirtualCameraWarning="После запуска Vertical виртуальной камеры вы не сможете изменить некоторые настройки OBS без перезапуска OBS."
NoOutputServer="Нет сервера вывода"
NoOutputServerWarning="Не найден сервер вывода. Убедитесь, что у вас включен вывод в настройках вертикального потока."
StartAll="Запустить все"
StopAll="Остановить все"
HelpIntro="Столкнулись с проблемой? Хотите узнать больше о Vertical? Ознакомьтесь с нашими справочными ресурсами ниже."
HelpTroubleshooterButton="Посетите веб-сайт Vertical средства устранения неполадок"
HelpGuideButton="Посетите веб-сайт руководства Vertical"
HelpDiscordButton="Посетите Aitum Discord"
StreamingMatchMain="Запуск и остановка потоковой передачи, когда OBS запускает и останавливает потоковую передачу"
RecordingMatchMain="Запуск и остановка записи, когда OBS запускает и останавливает запись"

63
data/locale/sv-SE.ini Normal file
View file

@ -0,0 +1,63 @@
Vertical="Vertikal"
VerticalCanvas="Vertikal Kanvas"
Description="Vertikal Kanvas"
Canvas="Kanvas"
VirtualCam="Virtuell kamera"
LinkedScenes="Länkade scener"
OnMainCanvas="På huvud arbetsytan"
SceneName="scen namn"
SourceName="käll namn"
TransitionName="Övergångs namn"
Saving="Sparar Backtrack..."
Saved="Backtrack Sparat!"
VerticalSettings="Vertikala inställningar"
General="Allmänt"
Resolution="Upplösning"
ShowScenes="Visa Vertikala scener i huvud scenlista"
Backtrack="Backtrack"
BacktrackEnable="Backtrack kör medans strömning/inspelning"
BacktrackAlwaysOn="Backtrack alltid på"
BacktrackDuration="Backtrack inspelningens varaktighet"
BacktrackPath="Backtrack filspar väg"
SaveBacktrackHotkey="Save Backtrack snabtangent"
Streaming="Strömning"
ViewGuide="Visa Guide"
Name="Namn"
Server="Server"
Key="Nyckel"
Enabled="Aktivera"
Output="Utgång"
StartStreamingHotkey="Snabbtangent: Starta ström"
StopStreamingHotkey="Snabbtangent: Stoppa ström"
Recording="Inspelning"
StartRecordingHotkey="Snabbtangent: Starta inspelning"
StopRecordingHotkey="Snabbtangent: Stoppa inspelning"
Version="Version"
MadeBy="Skapad med ♡ av"
AddVerticalScene="Lägg till vertikal scen"
RemoveVerticalScene="Ta bort vertikal scen"
StreamVertical="Strömma alla vertikala utgångar"
StreamVerticalMulti="Styr individuella vertikala utgångar"
RecordVertical="Spela in vertikalt"
BacktrackClipVertical="Backtrack klipp vertikalt"
VirtualCameraVertical="Vertikal virtuell kamera"
VerticalScene="Vertikal Scen"
tiktokError="Dessutom, eftersom vi har upptäckt att du försöker streama till TikTok, se till att din streamnyckel uppdateras när TikTok cyklar dem efter varje stream!"
backtrackStartFail="Backtrack start misslyckades"
backtrackCustomFfmpeg="Du kan inte aktivera Backtrack om inspelning är inställd på anpassad ffmpeg"
backtrackNoReplayBuffer="Du kan inte aktivera Backtrack om ingen återuppspelning inspelning hittas"
UseMain="Använd OBS huvud inställningar"
NewVersion="Ny version (%1) tillgänglig <a href='https://aitum.tv/download/vertical/'>här</a>"
RecordPathError="Den konfigurerade inspelningssökvägen är tom. Kontrollera din inspelningsväg under Vertikala inställningar → Inspelning."
Help="Hjälp"
HelpSupport="Gå med i Aitum Discord Server"
CopyFromMain="Kopiera från main"
VirtualCameraWarning="Efter att ha startat den vertikala virtuella kameran kanske du inte kan redigera vissa OBS-inställningar eftersom den tror att den fortfarande är aktiv efter att den stoppats. Starta om OBS för att kunna redigera inställningarna igen."
NoOutputServer="Ingen utdataserver"
NoOutputServerWarning="Det finns ingen aktiverad utdataserver. Se till att du har en utgång aktiverad i inställningarna för vertikal streaming."
StartAll="Starta allt"
StopAll="Stoppa allt"
HelpIntro="Stöter du på ett problem? Vill du lära dig mer om Vertical? Kolla in våra hjälpresurser nedan."
HelpTroubleshooterButton="Besök webbplatsen för Vertical Troubleshooter"
HelpGuideButton="Besök webbplatsen Vertical Guides"
HelpDiscordButton="Besök Aitum Discord"

72
data/locale/zh-CN.ini Normal file
View file

@ -0,0 +1,72 @@
Vertical="垂直"
VerticalCanvas="垂直画布"
Description="垂直画布"
Canvas="画布"
VirtualCam="虚拟摄像头"
LinkedScenes="关联场景"
OnMainCanvas="在主画布显示"
SceneName="场景名称"
SourceName="来源名称"
TransitionName="转场名称"
Saving="正在保存Backtrack..."
Saved="Backtrack已保存"
VerticalSettings="垂直设置"
General="通用"
Resolution="分辨率"
ShowScenes="在主场景列表中显示垂直场景"
Backtrack="Backtrack"
BacktrackEnable="在流媒体/录制时运行Backtrack"
BacktrackOn="Backtrack开启"
BacktrackOff="Backtrack关闭"
BacktrackDuration="Backtrack录制长度"
BacktrackPath="Backtrack录制路径"
StartBacktrackHotkey="开始Backtrack热键"
StopBacktrackHotkey="停止Backtrack热键"
SaveBacktrackHotkey="保存Backtrack热键"
Streaming="流媒体"
ViewGuide="查看指南"
Name="名称"
Server="服务器"
Key="密钥"
Enabled="启用"
Output="输出"
StartStreamingHotkey="开始流媒体热键"
StopStreamingHotkey="停止流媒体热键"
Recording="录制"
StartRecordingHotkey="开始录制热键"
StopRecordingHotkey="停止录制热键"
Version="版本"
MadeBy="由♡制作"
AddVerticalScene="添加垂直场景"
RemoveVerticalScene="删除垂直场景"
StreamVertical="流媒体所有垂直输出"
StreamVerticalMulti="控制单个垂直输出"
RecordVertical="录制垂直"
BacktrackClipVertical="Backtrack剪辑垂直"
VirtualCameraVertical="垂直虚拟摄像头"
VerticalScene="垂直场景"
tiktokError="此外因为我们检测到您正在尝试流媒体到TikTok请确保您的流媒体密钥已更新因为TikTok会在每次流媒体后循环它们"
backtrackStartFail="Backtrack启动失败"
backtrackCustomFfmpeg="当录制设置为自定义ffmpeg时无法启动Backtrack"
backtrackNoReplayBuffer="找不到重放缓冲区时无法启动Backtrack"
UseMain="使用主OBS设置"
NewVersion="新版本(%1)可用<a href='https://aitum.tv/download/vertical/'>在这里</a>"
RecordPathError="配置的录制路径为空。请在垂直设置→录制中检查您的录制路径。"
Help="帮助"
HelpSupport="加入Aitum Discord服务器"
CopyFromMain="从主复制"
VirtualCameraWarning="启动垂直虚拟摄像头后您可能无法编辑某些OBS设置因为它认为在停止后仍处于活动状态。重新启动OBS以再次编辑设置。"
NoOutputServer="没有输出服务器"
NoOutputServerWarning="找不到启用的输出服务器。请确保在垂直流媒体设置中启用了输出。"
StartAll="全部开始"
StopAll="全部停止"
HelpIntro="遇到问题?想了解更多关于垂直的信息?请查看下面的帮助资源。"
HelpTroubleshooterButton="访问垂直故障排除网站"
HelpGuideButton="访问垂直指南网站"
HelpDiscordButton="访问Aitum Discord"
VirtualCameraModeVertical="垂直"
VirtualCameraModeMain="主"
VirtualCameraModeBoth="两者"
StreamingMatchMain="当主OBS开始和停止流媒体时开始和停止流媒体"
RecordingMatchMain="当主OBS开始和停止录制时开始和停止录制"
OutputsMultistream="由于您正在使用Aitum Multistream您必须从Aitum Multistream设置中控制垂直流媒体输出。\n在此处禁用更改输出设置。"

138
display-helpers.hpp Normal file
View file

@ -0,0 +1,138 @@
/******************************************************************************
Copyright (C) 2014 by Hugh Bailey <obs.jim@gmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
#pragma once
#include <graphics/vec4.h>
#include <graphics/matrix4.h>
static inline void GetScaleAndCenterPos(int baseCX, int baseCY, int windowCX, int windowCY, int &x, int &y, float &scale)
{
double windowAspect, baseAspect;
int newCX, newCY;
windowAspect = double(windowCX) / double(windowCY);
baseAspect = double(baseCX) / double(baseCY);
if (windowAspect > baseAspect) {
scale = float(windowCY) / float(baseCY);
newCX = int(double(windowCY) * baseAspect);
newCY = windowCY;
} else {
scale = float(windowCX) / float(baseCX);
newCX = windowCX;
newCY = int(float(windowCX) / baseAspect);
}
x = windowCX / 2 - newCX / 2;
y = windowCY / 2 - newCY / 2;
}
static inline void GetCenterPosFromFixedScale(int baseCX, int baseCY, int windowCX, int windowCY, int &x, int &y, float scale)
{
x = (int)((float(windowCX) - float(baseCX) * scale) / 2.0f);
y = (int)((float(windowCY) - float(baseCY) * scale) / 2.0f);
}
static inline QSize GetPixelSize(QWidget *widget)
{
return widget->size() * widget->devicePixelRatioF();
}
#define OUTLINE_COLOR 0xFFD0D0D0
#define LINE_LENGTH 0.1f
// Rec. ITU-R BT.1848-1 / EBU R 95
#define ACTION_SAFE_PERCENT 0.035f // 3.5%
#define GRAPHICS_SAFE_PERCENT 0.05f // 5.0%
#define FOURBYTHREE_SAFE_PERCENT 0.1625f // 16.25%
static inline void InitSafeAreas(gs_vertbuffer_t **actionSafeMargin, gs_vertbuffer_t **graphicsSafeMargin,
gs_vertbuffer_t **fourByThreeSafeMargin, gs_vertbuffer_t **leftLine, gs_vertbuffer_t **topLine,
gs_vertbuffer_t **rightLine)
{
obs_enter_graphics();
// All essential action should be placed inside this area
gs_render_start(true);
gs_vertex2f(ACTION_SAFE_PERCENT, ACTION_SAFE_PERCENT);
gs_vertex2f(ACTION_SAFE_PERCENT, 1 - ACTION_SAFE_PERCENT);
gs_vertex2f(1 - ACTION_SAFE_PERCENT, 1 - ACTION_SAFE_PERCENT);
gs_vertex2f(1 - ACTION_SAFE_PERCENT, ACTION_SAFE_PERCENT);
gs_vertex2f(ACTION_SAFE_PERCENT, ACTION_SAFE_PERCENT);
*actionSafeMargin = gs_render_save();
// All graphics should be placed inside this area
gs_render_start(true);
gs_vertex2f(GRAPHICS_SAFE_PERCENT, GRAPHICS_SAFE_PERCENT);
gs_vertex2f(GRAPHICS_SAFE_PERCENT, 1 - GRAPHICS_SAFE_PERCENT);
gs_vertex2f(1 - GRAPHICS_SAFE_PERCENT, 1 - GRAPHICS_SAFE_PERCENT);
gs_vertex2f(1 - GRAPHICS_SAFE_PERCENT, GRAPHICS_SAFE_PERCENT);
gs_vertex2f(GRAPHICS_SAFE_PERCENT, GRAPHICS_SAFE_PERCENT);
*graphicsSafeMargin = gs_render_save();
// 4:3 safe area for widescreen
gs_render_start(true);
gs_vertex2f(FOURBYTHREE_SAFE_PERCENT, GRAPHICS_SAFE_PERCENT);
gs_vertex2f(1 - FOURBYTHREE_SAFE_PERCENT, GRAPHICS_SAFE_PERCENT);
gs_vertex2f(1 - FOURBYTHREE_SAFE_PERCENT, 1 - GRAPHICS_SAFE_PERCENT);
gs_vertex2f(FOURBYTHREE_SAFE_PERCENT, 1 - GRAPHICS_SAFE_PERCENT);
gs_vertex2f(FOURBYTHREE_SAFE_PERCENT, GRAPHICS_SAFE_PERCENT);
*fourByThreeSafeMargin = gs_render_save();
gs_render_start(true);
gs_vertex2f(0.0f, 0.5f);
gs_vertex2f(LINE_LENGTH, 0.5f);
*leftLine = gs_render_save();
gs_render_start(true);
gs_vertex2f(0.5f, 0.0f);
gs_vertex2f(0.5f, LINE_LENGTH);
*topLine = gs_render_save();
gs_render_start(true);
gs_vertex2f(1.0f, 0.5f);
gs_vertex2f(1 - LINE_LENGTH, 0.5f);
*rightLine = gs_render_save();
obs_leave_graphics();
}
static inline void RenderSafeAreas(gs_vertbuffer_t *vb, int cx, int cy)
{
if (!vb)
return;
matrix4 transform;
matrix4_identity(&transform);
transform.x.x = (float)cx;
transform.y.y = (float)cy;
gs_load_vertexbuffer(vb);
gs_matrix_push();
gs_matrix_mul(&transform);
gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID);
gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color");
gs_effect_set_color(color, OUTLINE_COLOR);
while (gs_effect_loop(solid, "Solid"))
gs_draw(GS_LINESTRIP, 0, 0);
gs_matrix_pop();
}

144
file-updater.c Normal file
View file

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

19
file-updater.h Normal file
View file

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

498
hotkey-edit.cpp Normal file
View file

@ -0,0 +1,498 @@
/******************************************************************************
Copyright (C) 2014-2015 by Ruwen Hahn <palana@stunned.de>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "hotkey-edit.hpp"
#include <util/dstr.hpp>
#include <QPointer>
#include <QStyle>
#include <QAction>
#include "obs-frontend-api.h"
#include "obs-module.h"
uint32_t TranslateQtKeyboardEventModifiers(Qt::KeyboardModifiers mods)
{
int obsModifiers = INTERACT_NONE;
if (mods.testFlag(Qt::ShiftModifier))
obsModifiers |= INTERACT_SHIFT_KEY;
if (mods.testFlag(Qt::AltModifier))
obsModifiers |= INTERACT_ALT_KEY;
#ifdef __APPLE__
// Mac: Meta = Control, Control = Command
if (mods.testFlag(Qt::ControlModifier))
obsModifiers |= INTERACT_COMMAND_KEY;
if (mods.testFlag(Qt::MetaModifier))
obsModifiers |= INTERACT_CONTROL_KEY;
#else
// Handle windows key? Can a browser even trap that key?
if (mods.testFlag(Qt::ControlModifier))
obsModifiers |= INTERACT_CONTROL_KEY;
if (mods.testFlag(Qt::MetaModifier))
obsModifiers |= INTERACT_COMMAND_KEY;
#endif
return obsModifiers;
}
void OBSHotkeyEdit::keyPressEvent(QKeyEvent *event)
{
if (event->isAutoRepeat())
return;
obs_key_combination_t new_key;
switch (event->key()) {
case Qt::Key_Shift:
case Qt::Key_Control:
case Qt::Key_Alt:
case Qt::Key_Meta:
new_key.key = OBS_KEY_NONE;
break;
#ifdef __APPLE__
case Qt::Key_CapsLock:
// kVK_CapsLock == 57
new_key.key = obs_key_from_virtual_key(57);
break;
#endif
default:
new_key.key = obs_key_from_virtual_key(event->nativeVirtualKey());
}
new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers());
HandleNewKey(new_key);
}
QVariant OBSHotkeyEdit::inputMethodQuery(Qt::InputMethodQuery query) const
{
if (query == Qt::ImEnabled) {
return false;
} else {
return QLineEdit::inputMethodQuery(query);
}
}
#ifdef __APPLE__
void OBSHotkeyEdit::keyReleaseEvent(QKeyEvent *event)
{
if (event->isAutoRepeat())
return;
if (event->key() != Qt::Key_CapsLock)
return;
obs_key_combination_t new_key;
// kVK_CapsLock == 57
new_key.key = obs_key_from_virtual_key(57);
new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers());
HandleNewKey(new_key);
}
#endif
void OBSHotkeyEdit::mousePressEvent(QMouseEvent *event)
{
obs_key_combination_t new_key;
switch (event->button()) {
case Qt::NoButton:
case Qt::LeftButton:
case Qt::RightButton:
case Qt::AllButtons:
case Qt::MouseButtonMask:
return;
case Qt::MiddleButton:
new_key.key = OBS_KEY_MOUSE3;
break;
#define MAP_BUTTON(i, j) \
case Qt::ExtraButton##i: \
new_key.key = OBS_KEY_MOUSE##j; \
break;
MAP_BUTTON(1, 4)
MAP_BUTTON(2, 5)
MAP_BUTTON(3, 6)
MAP_BUTTON(4, 7)
MAP_BUTTON(5, 8)
MAP_BUTTON(6, 9)
MAP_BUTTON(7, 10)
MAP_BUTTON(8, 11)
MAP_BUTTON(9, 12)
MAP_BUTTON(10, 13)
MAP_BUTTON(11, 14)
MAP_BUTTON(12, 15)
MAP_BUTTON(13, 16)
MAP_BUTTON(14, 17)
MAP_BUTTON(15, 18)
MAP_BUTTON(16, 19)
MAP_BUTTON(17, 20)
MAP_BUTTON(18, 21)
MAP_BUTTON(19, 22)
MAP_BUTTON(20, 23)
MAP_BUTTON(21, 24)
MAP_BUTTON(22, 25)
MAP_BUTTON(23, 26)
MAP_BUTTON(24, 27)
#undef MAP_BUTTON
}
new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers());
HandleNewKey(new_key);
}
void OBSHotkeyEdit::HandleNewKey(obs_key_combination_t new_key)
{
if (new_key == key || obs_key_combination_is_empty(new_key))
return;
key = new_key;
changed = true;
emit KeyChanged(key);
RenderKey();
}
void OBSHotkeyEdit::RenderKey()
{
DStr str;
obs_key_combination_to_str(key, str);
setText(QString::fromUtf8(str->array));
}
void OBSHotkeyEdit::ResetKey()
{
key = original;
changed = false;
emit KeyChanged(key);
RenderKey();
}
void OBSHotkeyEdit::ClearKey()
{
key = {0, OBS_KEY_NONE};
changed = true;
emit KeyChanged(key);
RenderKey();
}
void OBSHotkeyEdit::UpdateDuplicationState()
{
if (dupeIcon && dupeIcon->isVisible() != hasDuplicate) {
dupeIcon->setVisible(hasDuplicate);
update();
}
}
void OBSHotkeyEdit::InitSignalHandler()
{
layoutChanged = {obs_get_signal_handler(), "hotkey_layout_change",
[](void *this_, calldata_t *) {
auto edit = static_cast<OBSHotkeyEdit *>(this_);
QMetaObject::invokeMethod(edit, "ReloadKeyLayout");
},
this};
}
void OBSHotkeyEdit::CreateDupeIcon()
{
dupeIcon = addAction(QIcon::fromTheme("obs", QIcon(":/res/images/warning.svg")), ActionPosition::TrailingPosition);
dupeIcon->setToolTip(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Hotkeys.DuplicateWarning")));
QObject::connect(dupeIcon, &QAction::triggered, [=] { emit SearchKey(key); });
dupeIcon->setVisible(false);
}
void OBSHotkeyEdit::ReloadKeyLayout()
{
RenderKey();
}
void OBSHotkeyWidget::SetKeyCombinations(const std::vector<obs_key_combination_t> &combos)
{
if (combos.empty())
AddEdit({0, OBS_KEY_NONE});
for (auto combo : combos)
AddEdit(combo);
}
bool OBSHotkeyWidget::Changed() const
{
return changed || std::any_of(begin(edits), end(edits), [](OBSHotkeyEdit *edit) { return edit->changed; });
}
void OBSHotkeyWidget::Apply()
{
for (auto &edit : edits) {
edit->original = edit->key;
edit->changed = false;
}
changed = false;
for (auto &revertButton : revertButtons)
revertButton->setEnabled(false);
}
void OBSHotkeyWidget::GetCombinations(std::vector<obs_key_combination_t> &combinations) const
{
combinations.clear();
for (auto &edit : edits)
if (!obs_key_combination_is_empty(edit->key))
combinations.emplace_back(edit->key);
}
void OBSHotkeyWidget::Save()
{
std::vector<obs_key_combination_t> combinations;
Save(combinations);
}
void OBSHotkeyWidget::Save(std::vector<obs_key_combination_t> &combinations)
{
GetCombinations(combinations);
Apply();
auto AtomicUpdate = [&]() {
ignoreChangedBindings = true;
obs_hotkey_load_bindings(id, combinations.data(), combinations.size());
ignoreChangedBindings = false;
};
using AtomicUpdate_t = decltype(&AtomicUpdate);
obs_hotkey_update_atomic([](void *d) { (*static_cast<AtomicUpdate_t>(d))(); }, static_cast<void *>(&AtomicUpdate));
}
void OBSHotkeyWidget::AddEdit(obs_key_combination combo, int idx)
{
auto edit = new OBSHotkeyEdit(parentWidget(), combo);
edit->setToolTip(toolTip);
auto revert = new QPushButton;
revert->setProperty("themeID", "revertIcon");
revert->setProperty("class", "icon-revert");
revert->setToolTip(QString::fromUtf8(obs_frontend_get_locale_string("Revert")));
revert->setEnabled(false);
auto clear = new QPushButton;
clear->setProperty("themeID", "clearIconSmall");
clear->setProperty("class", "icon-trash");
clear->setToolTip(QString::fromUtf8(obs_frontend_get_locale_string("Clear")));
clear->setEnabled(!obs_key_combination_is_empty(combo));
QObject::connect(edit, &OBSHotkeyEdit::KeyChanged, [=](obs_key_combination_t new_combo) {
clear->setEnabled(!obs_key_combination_is_empty(new_combo));
revert->setEnabled(edit->original != new_combo);
});
auto add = new QPushButton;
add->setProperty("themeID", "addIconSmall");
add->setProperty("class", "icon-plus");
add->setToolTip(QString::fromUtf8(obs_frontend_get_locale_string("Add")));
auto remove = new QPushButton;
remove->setProperty("themeID", "removeIconSmall");
remove->setProperty("class", "icon-minus");
remove->setToolTip(QString::fromUtf8(obs_frontend_get_locale_string("Remove")));
remove->setEnabled(removeButtons.size() > 0);
auto CurrentIndex = [&, remove] {
auto res = std::find(begin(removeButtons), end(removeButtons), remove);
return std::distance(begin(removeButtons), res);
};
QObject::connect(add, &QPushButton::clicked, [&, CurrentIndex] { AddEdit({0, OBS_KEY_NONE}, (int)CurrentIndex() + 1); });
QObject::connect(remove, &QPushButton::clicked, [&, CurrentIndex] { RemoveEdit(CurrentIndex()); });
QHBoxLayout *subLayout = new QHBoxLayout;
subLayout->setContentsMargins(0, 4, 0, 0);
subLayout->addWidget(edit);
subLayout->addWidget(revert);
subLayout->addWidget(clear);
subLayout->addWidget(add);
subLayout->addWidget(remove);
if (removeButtons.size() == 1)
removeButtons.front()->setEnabled(true);
if (idx != -1) {
revertButtons.insert(begin(revertButtons) + idx, revert);
removeButtons.insert(begin(removeButtons) + idx, remove);
edits.insert(begin(edits) + idx, edit);
} else {
revertButtons.emplace_back(revert);
removeButtons.emplace_back(remove);
edits.emplace_back(edit);
}
layout()->insertLayout(idx, subLayout);
QObject::connect(revert, &QPushButton::clicked, edit, &OBSHotkeyEdit::ResetKey);
QObject::connect(clear, &QPushButton::clicked, edit, &OBSHotkeyEdit::ClearKey);
QObject::connect(edit, &OBSHotkeyEdit::KeyChanged, [&](obs_key_combination) { emit KeyChanged(); });
QObject::connect(edit, &OBSHotkeyEdit::SearchKey, [=](obs_key_combination c) { emit SearchKey(c); });
}
void OBSHotkeyWidget::RemoveEdit(size_t idx, bool signal)
{
auto &edit = *(begin(edits) + idx);
if (!obs_key_combination_is_empty(edit->original) && signal) {
changed = true;
}
revertButtons.erase(begin(revertButtons) + idx);
removeButtons.erase(begin(removeButtons) + idx);
edits.erase(begin(edits) + idx);
auto item = layout()->takeAt(static_cast<int>(idx));
QLayoutItem *child = nullptr;
while ((child = item->layout()->takeAt(0))) {
delete child->widget();
delete child;
}
delete item;
if (removeButtons.size() == 1)
removeButtons.front()->setEnabled(false);
emit KeyChanged();
}
void OBSHotkeyWidget::BindingsChanged(void *data, calldata_t *param)
{
auto widget = static_cast<OBSHotkeyWidget *>(data);
auto key = static_cast<obs_hotkey_t *>(calldata_ptr(param, "key"));
QMetaObject::invokeMethod(widget, "HandleChangedBindings", Q_ARG(obs_hotkey_id, obs_hotkey_get_id(key)));
}
void OBSHotkeyWidget::HandleChangedBindings(obs_hotkey_id id_)
{
if (ignoreChangedBindings || id != id_)
return;
std::vector<obs_key_combination_t> bindings;
auto LoadBindings = [&](obs_hotkey_binding_t *binding) {
if (obs_hotkey_binding_get_hotkey_id(binding) != id)
return;
auto get_combo = obs_hotkey_binding_get_key_combination;
bindings.push_back(get_combo(binding));
};
using LoadBindings_t = decltype(&LoadBindings);
obs_enum_hotkey_bindings(
[](void *param, size_t, obs_hotkey_binding_t *binding) {
auto LoadBindingsFunc = *static_cast<LoadBindings_t>(param);
LoadBindingsFunc(binding);
return true;
},
static_cast<void *>(&LoadBindings));
while (edits.size() > 0)
RemoveEdit(edits.size() - 1, false);
SetKeyCombinations(bindings);
}
static inline void updateStyle(QWidget *widget)
{
auto style = widget->style();
style->unpolish(widget);
style->polish(widget);
widget->update();
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void OBSHotkeyWidget::enterEvent(QEnterEvent *event)
#else
void OBSHotkeyWidget::enterEvent(QEvent *event)
#endif
{
if (!label)
return;
event->accept();
label->highlightPair(true);
}
void OBSHotkeyWidget::leaveEvent(QEvent *event)
{
if (!label)
return;
event->accept();
label->highlightPair(false);
}
void OBSHotkeyLabel::highlightPair(bool highlight)
{
if (!pairPartner)
return;
pairPartner->setProperty("hotkeyPairHover", highlight);
updateStyle(pairPartner);
setProperty("hotkeyPairHover", highlight);
updateStyle(this);
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void OBSHotkeyLabel::enterEvent(QEnterEvent *event)
#else
void OBSHotkeyLabel::enterEvent(QEvent *event)
#endif
{
if (!pairPartner)
return;
event->accept();
highlightPair(true);
}
void OBSHotkeyLabel::leaveEvent(QEvent *event)
{
if (!pairPartner)
return;
event->accept();
highlightPair(false);
}
void OBSHotkeyLabel::setToolTip(const QString &toolTip)
{
QLabel::setToolTip(toolTip);
if (widget)
widget->setToolTip(toolTip);
}

178
hotkey-edit.hpp Normal file
View file

@ -0,0 +1,178 @@
/******************************************************************************
Copyright (C) 2014-2015 by Ruwen Hahn <palana@stunned.de>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
#pragma once
#include <QLineEdit>
#include <QKeyEvent>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
#include <QPointer>
#include <QLabel>
#include <obs.hpp>
static inline bool operator!=(const obs_key_combination_t &c1, const obs_key_combination_t &c2)
{
return c1.modifiers != c2.modifiers || c1.key != c2.key;
}
static inline bool operator==(const obs_key_combination_t &c1, const obs_key_combination_t &c2)
{
return !(c1 != c2);
}
class OBSHotkeyWidget;
class OBSHotkeyLabel : public QLabel {
Q_OBJECT
public:
QPointer<OBSHotkeyLabel> pairPartner;
QPointer<OBSHotkeyWidget> widget;
void highlightPair(bool highlight);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void enterEvent(QEnterEvent *event) override;
#else
void enterEvent(QEvent *event) override;
#endif
void leaveEvent(QEvent *event) override;
void setToolTip(const QString &toolTip);
};
class OBSHotkeyEdit : public QLineEdit {
Q_OBJECT;
public:
OBSHotkeyEdit(QWidget *parent, obs_key_combination_t original_) : QLineEdit(parent), original(original_)
{
#ifdef __APPLE__
// disable the input cursor on OSX, focus should be clear
// enough with the default focus frame
setReadOnly(true);
#endif
setAttribute(Qt::WA_InputMethodEnabled, false);
setAttribute(Qt::WA_MacShowFocusRect, true);
InitSignalHandler();
CreateDupeIcon();
ResetKey();
}
obs_key_combination_t original;
obs_key_combination_t key;
bool changed = false;
void UpdateDuplicationState();
bool hasDuplicate = false;
QVariant inputMethodQuery(Qt::InputMethodQuery) const override;
protected:
OBSSignal layoutChanged;
QAction *dupeIcon;
void InitSignalHandler();
void CreateDupeIcon();
void keyPressEvent(QKeyEvent *event) override;
#ifdef __APPLE__
void keyReleaseEvent(QKeyEvent *event) override;
#endif
void mousePressEvent(QMouseEvent *event) override;
void RenderKey();
public slots:
void HandleNewKey(obs_key_combination_t new_key);
void ReloadKeyLayout();
void ResetKey();
void ClearKey();
signals:
void KeyChanged(obs_key_combination_t);
void SearchKey(obs_key_combination_t);
};
class OBSHotkeyWidget : public QWidget {
Q_OBJECT;
public:
OBSHotkeyWidget(QWidget *parent, obs_hotkey_id id_, std::string name_, const std::vector<obs_key_combination_t> &combos = {})
: QWidget(parent),
id(id_),
name(name_),
bindingsChanged(obs_get_signal_handler(), "hotkey_bindings_changed", &OBSHotkeyWidget::BindingsChanged, this)
{
auto layout = new QVBoxLayout;
layout->setSpacing(0);
layout->setContentsMargins(0, 0, 0, 0);
setLayout(layout);
SetKeyCombinations(combos);
}
void SetKeyCombinations(const std::vector<obs_key_combination_t> &);
obs_hotkey_id id;
std::string name;
bool changed = false;
bool Changed() const;
QPointer<OBSHotkeyLabel> label;
std::vector<QPointer<OBSHotkeyEdit>> edits;
QString toolTip;
void setToolTip(const QString &toolTip_)
{
toolTip = toolTip_;
for (auto &edit : edits)
edit->setToolTip(toolTip_);
}
void Apply();
void GetCombinations(std::vector<obs_key_combination_t> &) const;
void Save();
void Save(std::vector<obs_key_combination_t> &combinations);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void enterEvent(QEnterEvent *event) override;
#else
void enterEvent(QEvent *event) override;
#endif
void leaveEvent(QEvent *event) override;
private:
void AddEdit(obs_key_combination combo, int idx = -1);
void RemoveEdit(size_t idx, bool signal = true);
static void BindingsChanged(void *data, calldata_t *param);
std::vector<QPointer<QPushButton>> removeButtons;
std::vector<QPointer<QPushButton>> revertButtons;
OBSSignal bindingsChanged;
bool ignoreChangedBindings = false;
QVBoxLayout *layout() const { return dynamic_cast<QVBoxLayout *>(QWidget::layout()); }
private slots:
void HandleChangedBindings(obs_hotkey_id id_);
signals:
void KeyChanged();
void SearchKey(obs_key_combination_t);
};

BIN
media/aitum.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

5
media/backtrack_off.svg Normal file
View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="-4 2 48 48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path fill="#ff0000" stroke="none" d="M 28 26 C 28 21.581722 24.418278 18 20 18 C 15.581722 18 12 21.581722 12 26 C 12 30.418278 15.581722 34 20 34 C 24.418278 34 28 30.418278 28 26 Z"/>
<path fill="#1a57ff" stroke="none" d="M 20.111328 49.5 L 20.111328 45.998047 C 20.074188 45.998226 20.037188 46 20 46 C 8.954362 46 0 37.045639 0 26 C -0 20.389647 2.31037 15.319458 6.03125 11.6875 L 10.273438 15.929688 C 7.638605 18.475418 6 22.04689 6 26 C 6 33.731949 12.268052 40 20 40 C 20.03726 40 20.074121 39.998337 20.111328 39.998047 L 20.111328 36.5 L 27.888672 42.759766 L 20.111328 49.5 Z M 33.96875 40.3125 L 29.726563 36.070313 C 32.361397 33.524574 34 29.95311 34 26 C 34 18.268051 27.731949 12 20 12 C 19.96274 12 19.925879 12.001663 19.888672 12.001953 L 19.888672 15.5 L 12.111328 8.759766 L 19.888672 2.5 L 19.888672 6.001953 C 19.925812 6.001774 19.962812 6 20 6 C 31.045637 6 40 14.954361 40 26 C 40 31.610353 37.689636 36.680534 33.96875 40.3125 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

5
media/backtrack_on.svg Normal file
View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="-4 2 48 48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path fill="#ff0000" stroke="none" d="M 28 26 C 28 21.581722 24.418278 18 20 18 C 15.581722 18 12 21.581722 12 26 C 12 30.418278 15.581722 34 20 34 C 24.418278 34 28 30.418278 28 26 Z"/>
<path fill="#ffffff" stroke="none" d="M 20.111328 49.5 L 20.111328 45.998047 C 20.074188 45.998226 20.037188 46 20 46 C 8.954362 46 0 37.045639 0 26 C -0 20.389647 2.31037 15.319458 6.03125 11.6875 L 10.273438 15.929688 C 7.638605 18.475418 6 22.04689 6 26 C 6 33.731949 12.268052 40 20 40 C 20.03726 40 20.074121 39.998337 20.111328 39.998047 L 20.111328 36.5 L 27.888672 42.759766 L 20.111328 49.5 Z M 33.96875 40.3125 L 29.726563 36.070313 C 32.361397 33.524574 34 29.95311 34 26 C 34 18.268051 27.731949 12 20 12 C 19.96274 12 19.925879 12.001663 19.888672 12.001953 L 19.888672 15.5 L 12.111328 8.759766 L 19.888672 2.5 L 19.888672 6.001953 C 19.925812 6.001774 19.962812 6 20 6 C 31.045637 6 40 14.954361 40 26 C 40 31.610353 37.689636 36.680534 33.96875 40.3125 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
media/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
media/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

37
media/record.svg Normal file
View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8"
version="1.1"
viewBox="0 0 2.1166666 2.1166667"
height="8"
width="8">
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(0,-294.88332)"
id="layer1">
<circle
r="0.52916664"
cy="295.94165"
cx="1.0583333"
id="path4544"
style="fill:#ff0000;fill-opacity:1;stroke:none;stroke-width:0.06614584;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

37
media/recording.svg Normal file
View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8"
version="1.1"
viewBox="0 0 2.1166666 2.1166667"
height="8"
width="8">
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(0,-294.88332)"
id="layer1">
<circle
r="0.52916664"
cy="295.94165"
cx="1.0583333"
id="path4544"
style="fill:#FFFFFF;fill-opacity:1;stroke:none;stroke-width:0.06614584;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

55
media/stream.svg Normal file
View file

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

After

Width:  |  Height:  |  Size: 2.7 KiB

55
media/streaming.svg Normal file
View file

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

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 3.011719 1 c -1.652344 0 -3.011719 1.359375 -3.011719 3.011719 v 4.976562 c 0 1.652344 1.359375 3.011719 3.011719 3.011719 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 c -0.578125 0 -1.011719 -0.433594 -1.011719 -1.011719 v -4.976562 c 0 -0.578125 0.433594 -1.011719 1.011719 -1.011719 h 7.976562 c 0.578125 0 1.011719 0.433594 1.011719 1.011719 v 1.953125 c 0 0.554687 0.449219 1 1 1 s 1 -0.445313 1 -1 v -1.953125 c 0 -1.652344 -1.359375 -3.011719 -3.011719 -3.011719 z m 3.917969 7.011719 c -1.070313 0 -1.929688 0.863281 -1.929688 1.929687 v 3.140625 c 0 1.070313 0.859375 1.929688 1.929688 1.929688 h 4.140624 c 1.070313 0 1.929688 -0.859375 1.929688 -1.929688 v -0.578125 l 1.851562 1.378906 c 0.214844 0.160157 0.5 0.183594 0.738282 0.066407 c 0.242187 -0.121094 0.390625 -0.367188 0.390625 -0.636719 v -3.601562 c 0.003906 -0.269532 -0.148438 -0.515626 -0.390625 -0.636719 c -0.238282 -0.121094 -0.527344 -0.09375 -0.742188 0.066406 l -1.847656 1.371094 v -0.570313 c 0 -1.066406 -0.859375 -1.929687 -1.929688 -1.929687 z m 0 0" fill="#C08000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

4
media/virtual_cam_on.svg Normal file
View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 3.011719 1 c -1.652344 0 -3.011719 1.359375 -3.011719 3.011719 v 4.976562 c 0 1.652344 1.359375 3.011719 3.011719 3.011719 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 c -0.578125 0 -1.011719 -0.433594 -1.011719 -1.011719 v -4.976562 c 0 -0.578125 0.433594 -1.011719 1.011719 -1.011719 h 7.976562 c 0.578125 0 1.011719 0.433594 1.011719 1.011719 v 1.953125 c 0 0.554687 0.449219 1 1 1 s 1 -0.445313 1 -1 v -1.953125 c 0 -1.652344 -1.359375 -3.011719 -3.011719 -3.011719 z m 3.917969 7.011719 c -1.070313 0 -1.929688 0.863281 -1.929688 1.929687 v 3.140625 c 0 1.070313 0.859375 1.929688 1.929688 1.929688 h 4.140624 c 1.070313 0 1.929688 -0.859375 1.929688 -1.929688 v -0.578125 l 1.851562 1.378906 c 0.214844 0.160157 0.5 0.183594 0.738282 0.066407 c 0.242187 -0.121094 0.390625 -0.367188 0.390625 -0.636719 v -3.601562 c 0.003906 -0.269532 -0.148438 -0.515626 -0.390625 -0.636719 c -0.238282 -0.121094 -0.527344 -0.09375 -0.742188 0.066406 l -1.847656 1.371094 v -0.570313 c 0 -1.066406 -0.859375 -1.929687 -1.929688 -1.929687 z m 0 0" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

198
multi-canvas-source.c Normal file
View file

@ -0,0 +1,198 @@
#include <obs-module.h>
#include "multi-canvas-source.h"
struct multi_canvas_info {
obs_source_t *source;
uint32_t width;
uint32_t height;
DARRAY(obs_canvas_t *) canvas;
DARRAY(uint32_t) widths;
DARRAY(uint32_t) heights;
DARRAY(gs_texrender_t *) renders;
};
const char *multi_canvas_get_name(void *type_data)
{
UNUSED_PARAMETER(type_data);
return "vertical_multi_canvas";
}
void *multi_canvas_create(obs_data_t *settings, obs_source_t *source)
{
UNUSED_PARAMETER(settings);
struct multi_canvas_info *multi_canvas = bzalloc(sizeof(struct multi_canvas_info));
multi_canvas->source = source;
return multi_canvas;
}
void multi_canvas_destroy(void *data)
{
struct multi_canvas_info *mc = data;
da_free(mc->canvas);
da_free(mc->widths);
da_free(mc->heights);
for (size_t i = 0; i < mc->renders.num; i++) {
gs_texrender_destroy(mc->renders.array[i]);
}
da_free(mc->renders);
bfree(data);
}
bool view_get_width(void *data, struct obs_video_info *ovi)
{
uint32_t *width = data;
if (ovi->base_width > *width)
*width = ovi->base_width;
return true;
}
bool view_get_height(void *data, struct obs_video_info *ovi)
{
uint32_t *height = data;
if (ovi->base_height > *height)
*height = ovi->base_height;
return true;
}
static void multi_canvas_video_render(void *data, gs_effect_t *effect)
{
struct multi_canvas_info *mc = data;
gs_matrix_push();
for (uint32_t i = 0; i < MAX_CHANNELS; i++) {
obs_source_t *s = obs_get_output_source(i);
if (!s)
continue;
if (obs_source_removed(s)) {
obs_source_release(s);
continue;
}
gs_matrix_push();
gs_blend_state_push();
obs_source_video_render(s);
gs_blend_state_pop();
gs_matrix_pop();
obs_source_release(s);
}
struct obs_video_info ovi;
obs_get_video_info(&ovi);
gs_matrix_translate3f((float)ovi.base_width, 0.0f, 0.0f);
effect = obs_get_base_effect(OBS_EFFECT_DEFAULT);
for (size_t i = 0; i < mc->canvas.num; i++) {
obs_canvas_t *canvas = mc->canvas.array[i];
const enum gs_color_format format = gs_get_format_from_space(gs_get_color_space());
if (gs_texrender_get_format(mc->renders.array[i]) != format) {
gs_texrender_destroy(mc->renders.array[i]);
mc->renders.array[i] = gs_texrender_create(format, GS_ZS_NONE);
}
gs_texrender_reset(mc->renders.array[i]);
gs_blend_state_push();
gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO);
if (gs_texrender_begin_with_color_space(mc->renders.array[i], mc->widths.array[i], mc->heights.array[i],
gs_get_color_space())) {
struct vec4 clear_color;
vec4_zero(&clear_color);
gs_clear(GS_CLEAR_COLOR, &clear_color, 0.0f, 0);
gs_ortho(0.0f, (float)mc->widths.array[i], 0.0f, (float)mc->heights.array[i], -100.0f, 100.0f);
obs_canvas_render(canvas);
gs_texrender_end(mc->renders.array[i]);
}
gs_blend_state_pop();
gs_texture_t *tex = gs_texrender_get_texture(mc->renders.array[i]);
if (tex) {
const bool previous = gs_framebuffer_srgb_enabled();
gs_enable_framebuffer_srgb(true);
gs_effect_set_texture_srgb(gs_effect_get_param_by_name(effect, "image"), tex);
while (gs_effect_loop(effect, "Draw"))
gs_draw_sprite(tex, 0, mc->widths.array[i], mc->heights.array[i]);
gs_enable_framebuffer_srgb(previous);
}
gs_matrix_translate3f((float)mc->widths.array[i], 0.0f, 0.0f);
}
gs_matrix_pop();
}
uint32_t multi_canvas_get_width(void *data)
{
struct multi_canvas_info *mc = data;
return mc->width;
}
uint32_t multi_canvas_get_height(void *data)
{
struct multi_canvas_info *mc = data;
return mc->height;
}
void multi_canvas_update_size(struct multi_canvas_info *mc)
{
struct obs_video_info ovi;
obs_get_video_info(&ovi);
uint32_t width = ovi.base_width;
uint32_t height = ovi.base_height;
for (size_t i = 0; i < mc->widths.num; i++) {
width += mc->widths.array[i];
if (mc->heights.array[i] > height)
height = mc->heights.array[i];
}
mc->width = width;
mc->height = height;
}
void multi_canvas_source_add_canvas(void *data, obs_canvas_t *canvas, uint32_t width, uint32_t height)
{
struct multi_canvas_info *mc = data;
for (size_t i = 0; i < mc->canvas.num; i++) {
if (mc->canvas.array[i] == canvas)
return;
}
da_push_back(mc->widths, &width);
da_push_back(mc->heights, &height);
da_push_back(mc->canvas, &canvas);
gs_texrender_t *render = gs_texrender_create(GS_RGBA, GS_ZS_NONE);
da_push_back(mc->renders, &render);
multi_canvas_update_size(mc);
}
void multi_canvas_source_remove_canvas(void *data, obs_canvas_t *canvas)
{
struct multi_canvas_info *mc = data;
for (size_t i = 0; i < mc->canvas.num; i++) {
if (mc->canvas.array[i] == canvas) {
gs_texrender_destroy(mc->renders.array[i]);
da_erase(mc->canvas, i);
da_erase(mc->widths, i);
da_erase(mc->heights, i);
da_erase(mc->renders, i);
break;
}
}
multi_canvas_update_size(mc);
}
struct obs_source_info multi_canvas_source = {
.id = "vertical_multi_canvas_source",
.type = OBS_SOURCE_TYPE_INPUT,
.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CAP_DISABLED | OBS_SOURCE_CUSTOM_DRAW,
.get_name = multi_canvas_get_name,
.create = multi_canvas_create,
.destroy = multi_canvas_destroy,
.video_render = multi_canvas_video_render,
.get_width = multi_canvas_get_width,
.get_height = multi_canvas_get_height,
};

16
multi-canvas-source.h Normal file
View file

@ -0,0 +1,16 @@
#pragma once
#include <util/darray.h>
#ifdef __cplusplus
extern "C" {
#endif
void multi_canvas_source_add_canvas(void *data, obs_canvas_t *canvas, uint32_t width, uint32_t height);
void multi_canvas_source_remove_canvas(void *data, obs_canvas_t *canvas);
extern struct obs_source_info multi_canvas_source;
#ifdef __cplusplus
};
#endif

53
name-dialog.cpp Normal file
View file

@ -0,0 +1,53 @@
#include "name-dialog.hpp"
#include <QVBoxLayout>
#include "obs-module.h"
NameDialog::NameDialog(QWidget *parent, const QString &title) : QDialog(parent)
{
setWindowTitle(title);
setModal(true);
setWindowModality(Qt::WindowModality::WindowModal);
setMinimumWidth(200);
setMinimumHeight(100);
QVBoxLayout *layout = new QVBoxLayout;
setLayout(layout);
userText = new QLineEdit(this);
layout->addWidget(userText);
QDialogButtonBox *buttonbox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
layout->addWidget(buttonbox);
buttonbox->setCenterButtons(true);
connect(buttonbox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonbox, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
static bool IsWhitespace(char ch)
{
return ch == ' ' || ch == '\t';
}
static void CleanWhitespace(std::string &str)
{
while (!str.empty() && IsWhitespace(str.back()))
str.erase(str.end() - 1);
while (!str.empty() && IsWhitespace(str.front()))
str.erase(str.begin());
}
bool NameDialog::AskForName(QWidget *parent, const QString &title, std::string &name)
{
NameDialog dialog(parent, title);
dialog.userText->setMaxLength(170);
dialog.userText->setText(QString::fromUtf8(name.c_str()));
dialog.userText->selectAll();
if (dialog.exec() != DialogCode::Accepted) {
return false;
}
name = dialog.userText->text().toUtf8().constData();
CleanWhitespace(name);
return true;
}

17
name-dialog.hpp Normal file
View file

@ -0,0 +1,17 @@
#pragma once
#include <QDialog>
#include <QLabel>
#include <QLineEdit>
#include <QDialogButtonBox>
#include <string>
class NameDialog : public QDialog {
Q_OBJECT
public:
static bool AskForName(QWidget *parent, const QString &title, std::string &name);
private:
NameDialog(QWidget *parent, const QString &title);
QLineEdit *userText;
};

216
obs-websocket-api.h Normal file
View file

@ -0,0 +1,216 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2022 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#ifndef _OBS_WEBSOCKET_API_H
#define _OBS_WEBSOCKET_API_H
#include <obs.h>
#define OBS_WEBSOCKET_API_VERSION 2
#ifdef __cplusplus
extern "C" {
#endif
typedef void *obs_websocket_vendor;
typedef void (*obs_websocket_request_callback_function)(obs_data_t *, obs_data_t *, void *);
struct obs_websocket_request_response {
unsigned int status_code;
char *comment;
char *response_data; // JSON string, because obs_data_t* only supports array<object>, so conversions would break API.
};
/* ==================== INTERNAL DEFINITIONS ==================== */
struct obs_websocket_request_callback {
obs_websocket_request_callback_function callback;
void *priv_data;
};
inline proc_handler_t *_ph;
/* ==================== INTERNAL API FUNCTIONS ==================== */
static inline proc_handler_t *obs_websocket_get_ph(void)
{
proc_handler_t *global_ph = obs_get_proc_handler();
assert(global_ph != NULL);
calldata_t cd = {0};
if (!proc_handler_call(global_ph, "obs_websocket_api_get_ph", &cd))
blog(LOG_DEBUG, "Unable to fetch obs-websocket proc handler object. obs-websocket not installed?");
proc_handler_t *ret = (proc_handler_t *)calldata_ptr(&cd, "ph");
calldata_free(&cd);
return ret;
}
static inline bool obs_websocket_ensure_ph(void)
{
if (!_ph)
_ph = obs_websocket_get_ph();
return _ph != NULL;
}
static inline bool obs_websocket_vendor_run_simple_proc(obs_websocket_vendor vendor, const char *proc_name, calldata_t *cd)
{
if (!obs_websocket_ensure_ph())
return false;
if (!vendor || !proc_name || !strlen(proc_name) || !cd)
return false;
calldata_set_ptr(cd, "vendor", vendor);
proc_handler_call(_ph, proc_name, cd);
return calldata_bool(cd, "success");
}
/* ==================== GENERAL API FUNCTIONS ==================== */
// Gets the API version built with the obs-websocket plugin
static inline unsigned int obs_websocket_get_api_version(void)
{
if (!obs_websocket_ensure_ph())
return 0;
calldata_t cd = {0};
if (!proc_handler_call(_ph, "get_api_version", &cd))
return 1; // API v1 does not include get_api_version
unsigned int ret = (unsigned int)calldata_int(&cd, "version");
calldata_free(&cd);
return ret;
}
// Calls an obs-websocket request. Free response with `obs_websocket_request_response_free()`
static inline obs_websocket_request_response *obs_websocket_call_request(const char *request_type, obs_data_t *request_data = NULL)
{
if (!obs_websocket_ensure_ph())
return NULL;
const char *request_data_string = NULL;
if (request_data)
request_data_string = obs_data_get_json(request_data);
calldata_t cd = {0};
calldata_set_string(&cd, "request_type", request_type);
calldata_set_string(&cd, "request_data", request_data_string);
proc_handler_call(_ph, "call_request", &cd);
auto ret = (struct obs_websocket_request_response *)calldata_ptr(&cd, "response");
calldata_free(&cd);
return ret;
}
// Free a request response object returned by `obs_websocket_call_request()`
static inline void obs_websocket_request_response_free(struct obs_websocket_request_response *response)
{
if (!response)
return;
if (response->comment)
bfree(response->comment);
if (response->response_data)
bfree(response->response_data);
bfree(response);
}
/* ==================== VENDOR API FUNCTIONS ==================== */
// ALWAYS CALL ONLY VIA `obs_module_post_load()` CALLBACK!
// Registers a new "vendor" (Example: obs-ndi)
static inline obs_websocket_vendor obs_websocket_register_vendor(const char *vendor_name)
{
if (!obs_websocket_ensure_ph())
return NULL;
calldata_t cd = {0};
calldata_set_string(&cd, "name", vendor_name);
proc_handler_call(_ph, "vendor_register", &cd);
obs_websocket_vendor ret = calldata_ptr(&cd, "vendor");
calldata_free(&cd);
return ret;
}
// Registers a new request for a vendor
static inline bool obs_websocket_vendor_register_request(obs_websocket_vendor vendor, const char *request_type,
obs_websocket_request_callback_function request_callback, void *priv_data)
{
calldata_t cd = {0};
struct obs_websocket_request_callback cb = {};
cb.callback = request_callback;
cb.priv_data = priv_data;
calldata_set_string(&cd, "type", request_type);
calldata_set_ptr(&cd, "callback", &cb);
const bool success = obs_websocket_vendor_run_simple_proc(vendor, "vendor_request_register", &cd);
calldata_free(&cd);
return success;
}
// Unregisters an existing vendor request
static inline bool obs_websocket_vendor_unregister_request(obs_websocket_vendor vendor, const char *request_type)
{
calldata_t cd = {0};
calldata_set_string(&cd, "type", request_type);
const bool success = obs_websocket_vendor_run_simple_proc(vendor, "vendor_request_unregister", &cd);
calldata_free(&cd);
return success;
}
// Does not affect event_data refcount.
// Emits an event under the vendor's name
static inline bool obs_websocket_vendor_emit_event(obs_websocket_vendor vendor, const char *event_name, obs_data_t *event_data)
{
calldata_t cd = {0};
calldata_set_string(&cd, "type", event_name);
calldata_set_ptr(&cd, "data", (void *)event_data);
const bool success = obs_websocket_vendor_run_simple_proc(vendor, "vendor_event_emit", &cd);
calldata_free(&cd);
return success;
}
/* ==================== END API FUNCTIONS ==================== */
#ifdef __cplusplus
}
#endif
#endif

435
projector.cpp Normal file
View file

@ -0,0 +1,435 @@
#include <QAction>
#include <QGuiApplication>
#include <QMouseEvent>
#include <QMenu>
#include <QScreen>
#include <QWindow>
#include "projector.hpp"
#include <obs-module.h>
#include <obs-frontend-api.h>
#include <util/config-file.h>
#include <util/platform.h>
#ifdef _WIN32
#include <windows.h>
#endif
static inline void GetScaleAndCenterPos(int baseCX, int baseCY, int windowCX, int windowCY, int &x, int &y, float &scale)
{
double windowAspect, baseAspect;
int newCX, newCY;
windowAspect = double(windowCX) / double(windowCY);
baseAspect = double(baseCX) / double(baseCY);
if (windowAspect > baseAspect) {
scale = float(windowCY) / float(baseCY);
newCX = int(double(windowCY) * baseAspect);
newCY = windowCY;
} else {
scale = float(windowCX) / float(baseCX);
newCX = windowCX;
newCY = int(float(windowCX) / baseAspect);
}
x = windowCX / 2 - newCX / 2;
y = windowCY / 2 - newCY / 2;
}
static inline void startRegion(int vX, int vY, int vCX, int vCY, float oL, float oR, float oT, float oB)
{
gs_projection_push();
gs_viewport_push();
gs_set_viewport(vX, vY, vCX, vCY);
gs_ortho(oL, oR, oT, oB, -100.0f, 100.0f);
}
static inline void endRegion()
{
gs_viewport_pop();
gs_projection_pop();
}
config_t *get_user_config(void);
OBSProjector::OBSProjector(CanvasDock *canvas_, int monitor) : OBSQTDisplay(nullptr, Qt::Window), canvas(canvas_)
{
isAlwaysOnTop = config_get_bool(get_user_config(), "BasicWindow", "ProjectorAlwaysOnTop");
if (isAlwaysOnTop)
setWindowFlags(Qt::WindowStaysOnTopHint);
// Mark the window as a projector so SetDisplayAffinity
// can skip it
windowHandle()->setProperty("isOBSProjectorWindow", true);
#if defined(__linux__) || defined(__FreeBSD__) || defined(__DragonFly__)
// Prevents resizing of projector windows
setAttribute(Qt::WA_PaintOnScreen, false);
#endif
#ifdef __APPLE__
setWindowIcon(QIcon::fromTheme("obs", QIcon(":/res/images/obs_256x256.png")));
#else
setWindowIcon(QIcon::fromTheme("obs", QIcon(":/res/images/obs.png")));
#endif
if (monitor == -1)
resize(480, 270);
else
SetMonitor(monitor);
UpdateProjectorTitle();
QAction *action = new QAction(this);
action->setShortcut(Qt::Key_Escape);
addAction(action);
connect(action, SIGNAL(triggered()), this, SLOT(EscapeTriggered()));
setAttribute(Qt::WA_DeleteOnClose, true);
//disable application quit when last window closed
setAttribute(Qt::WA_QuitOnClose, false);
//installEventFilter(CreateShortcutFilter());
auto addDrawCallback = [this]() {
obs_display_add_draw_callback(GetDisplay(), OBSRender, this);
obs_display_set_background_color(GetDisplay(), 0x000000);
};
connect(this, &OBSQTDisplay::DisplayCreated, addDrawCallback);
//connect(App(), &QGuiApplication::screenRemoved, this, &OBSProjector::ScreenRemoved);
//App()->IncrementSleepInhibition();
ready = true;
show();
// We need it here to allow keyboard input in X11 to listen to Escape
activateWindow();
}
OBSProjector::~OBSProjector()
{
obs_display_remove_draw_callback(GetDisplay(), OBSRender, this);
//App()->DecrementSleepInhibition();
screen = nullptr;
}
void OBSProjector::SetMonitor(int monitor)
{
savedMonitor = monitor;
auto screens = QGuiApplication::screens();
if (monitor < 0 || monitor >= screens.size())
screen = nullptr;
else
screen = screens[monitor];
if (screen)
setGeometry(screen->geometry());
showFullScreen();
SetHideCursor();
}
void OBSProjector::SetHideCursor()
{
if (savedMonitor == -1)
return;
bool hideCursor = config_get_bool(get_user_config(), "BasicWindow", "HideProjectorCursor");
if (hideCursor)
setCursor(Qt::BlankCursor);
else
setCursor(Qt::ArrowCursor);
}
void OBSProjector::OBSRender(void *data, uint32_t cx, uint32_t cy)
{
OBSProjector *window = reinterpret_cast<OBSProjector *>(data);
if (!window->ready)
return;
obs_canvas_t *canvas = window->canvas->canvas;
uint32_t targetCX;
uint32_t targetCY;
int x, y;
int newCX, newCY;
float scale;
targetCX = window->canvas->canvas_width;
targetCY = window->canvas->canvas_height;
GetScaleAndCenterPos(targetCX, targetCY, cx, cy, x, y, scale);
newCX = int(scale * float(targetCX));
newCY = int(scale * float(targetCY));
startRegion(x, y, newCX, newCY, 0.0f, float(targetCX), 0.0f, float(targetCY));
obs_canvas_render(canvas);
endRegion();
}
void OBSProjector::mousePressEvent(QMouseEvent *event)
{
OBSQTDisplay::mousePressEvent(event);
if (event->button() == Qt::RightButton) {
QMenu popup(this);
QMenu *projectorMenu = new QMenu(QString::fromUtf8(obs_frontend_get_locale_string("Fullscreen")));
canvas->AddProjectorMenuMonitors(projectorMenu, this, SLOT(OpenFullScreenProjector()));
popup.addMenu(projectorMenu);
if (GetMonitor() > -1) {
popup.addAction(QString::fromUtf8(obs_frontend_get_locale_string("Windowed")), this,
SLOT(OpenWindowedProjector()));
} else if (!this->isMaximized()) {
popup.addAction(QString::fromUtf8(obs_frontend_get_locale_string("ResizeProjectorWindowToContent")), this,
SLOT(ResizeToContent()));
}
QAction *alwaysOnTopButton =
new QAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.View.AlwaysOnTop")), this);
alwaysOnTopButton->setCheckable(true);
alwaysOnTopButton->setChecked(isAlwaysOnTop);
connect(alwaysOnTopButton, &QAction::toggled, this, &OBSProjector::AlwaysOnTopToggled);
popup.addAction(alwaysOnTopButton);
popup.addAction(QString::fromUtf8(obs_frontend_get_locale_string("Close")), this, SLOT(EscapeTriggered()));
popup.exec(QCursor::pos());
}
}
void OBSProjector::EscapeTriggered()
{
canvas->DeleteProjector(this);
}
void OBSProjector::UpdateProjectorTitle(QString name)
{
UNUSED_PARAMETER(name);
bool window = (GetMonitor() == -1);
QString title = QString::fromUtf8(obs_frontend_get_locale_string(window ? "PreviewWindow" : "PreviewProjector"));
setWindowTitle(title);
}
int OBSProjector::GetMonitor()
{
return savedMonitor;
}
void OBSProjector::RenameProjector(QString oldName, QString newName)
{
if (oldName == newName)
return;
UpdateProjectorTitle(newName);
}
void OBSProjector::OpenFullScreenProjector()
{
if (!isFullScreen())
prevGeometry = geometry();
int monitor = sender()->property("monitor").toInt();
SetMonitor(monitor);
UpdateProjectorTitle();
}
void OBSProjector::OpenWindowedProjector()
{
showFullScreen();
showNormal();
setCursor(Qt::ArrowCursor);
if (!prevGeometry.isNull())
setGeometry(prevGeometry);
else
resize(480, 270);
savedMonitor = -1;
UpdateProjectorTitle();
screen = nullptr;
}
void OBSProjector::ResizeToContent()
{
uint32_t targetCX = canvas->canvas_width;
uint32_t targetCY = canvas->canvas_height;
int x, y, newX, newY;
float scale;
QSize size = this->size();
GetScaleAndCenterPos(targetCX, targetCY, size.width(), size.height(), x, y, scale);
newX = size.width() - (x * 2);
newY = size.height() - (y * 2);
resize(newX, newY);
}
void OBSProjector::AlwaysOnTopToggled(bool onTop)
{
SetIsAlwaysOnTop(onTop, true);
}
void OBSProjector::closeEvent(QCloseEvent *event)
{
EscapeTriggered();
event->accept();
}
bool OBSProjector::IsAlwaysOnTop() const
{
return isAlwaysOnTop;
}
bool OBSProjector::IsAlwaysOnTopOverridden() const
{
return isAlwaysOnTopOverridden;
}
void OBSProjector::SetIsAlwaysOnTop(bool onTop, bool isOverridden)
{
isAlwaysOnTop = onTop;
isAlwaysOnTopOverridden = isOverridden;
SetAlwaysOnTop(this, onTop);
}
void OBSProjector::ScreenRemoved(QScreen *screen_)
{
if (GetMonitor() < 0 || !screen)
return;
if (screen == screen_)
EscapeTriggered();
}
#ifdef _WIN32
bool IsAlwaysOnTop(QWidget *window)
{
DWORD exStyle = GetWindowLong((HWND)window->winId(), GWL_EXSTYLE);
return (exStyle & WS_EX_TOPMOST) != 0;
}
#else
bool IsAlwaysOnTop(QWidget *window)
{
return (window->windowFlags() & Qt::WindowStaysOnTopHint) != 0;
}
#endif
#ifdef _WIN32
void SetAlwaysOnTop(QWidget *window, bool enable)
{
HWND hwnd = (HWND)window->winId();
SetWindowPos(hwnd, enable ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
#else
void SetAlwaysOnTop(QWidget *window, bool enable)
{
Qt::WindowFlags flags = window->windowFlags();
if (enable) {
flags |= Qt::WindowStaysOnTopHint;
} else {
flags &= ~Qt::WindowStaysOnTopHint;
}
window->setWindowFlags(flags);
window->show();
}
#endif
#ifdef _WIN32
#if QT_VERSION < QT_VERSION_CHECK(6, 4, 0)
#define GENERIC_MONITOR_NAME QStringLiteral("Generic PnP Monitor")
struct MonitorData {
const wchar_t *id;
MONITORINFOEX info;
bool found;
};
static BOOL CALLBACK GetMonitorCallback(HMONITOR monitor, HDC, LPRECT, LPARAM param)
{
MonitorData *data = (MonitorData *)param;
if (GetMonitorInfoW(monitor, &data->info)) {
if (wcscmp(data->info.szDevice, data->id) == 0) {
data->found = true;
return false;
}
}
return true;
}
QString GetMonitorName(const QString &id)
{
MonitorData data = {};
data.id = (const wchar_t *)id.utf16();
data.info.cbSize = sizeof(data.info);
EnumDisplayMonitors(nullptr, nullptr, GetMonitorCallback, (LPARAM)&data);
if (!data.found) {
return GENERIC_MONITOR_NAME;
}
UINT32 numPath, numMode;
if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &numPath, &numMode) != ERROR_SUCCESS) {
return GENERIC_MONITOR_NAME;
}
std::vector<DISPLAYCONFIG_PATH_INFO> paths(numPath);
std::vector<DISPLAYCONFIG_MODE_INFO> modes(numMode);
if (QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &numPath, paths.data(), &numMode, modes.data(), nullptr) != ERROR_SUCCESS) {
return GENERIC_MONITOR_NAME;
}
DISPLAYCONFIG_TARGET_DEVICE_NAME target;
bool found = false;
paths.resize(numPath);
for (size_t i = 0; i < numPath; ++i) {
const DISPLAYCONFIG_PATH_INFO &path = paths[i];
DISPLAYCONFIG_SOURCE_DEVICE_NAME s;
s.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
s.header.size = sizeof(s);
s.header.adapterId = path.sourceInfo.adapterId;
s.header.id = path.sourceInfo.id;
if (DisplayConfigGetDeviceInfo(&s.header) == ERROR_SUCCESS &&
wcscmp(data.info.szDevice, s.viewGdiDeviceName) == 0) {
target.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME;
target.header.size = sizeof(target);
target.header.adapterId = path.sourceInfo.adapterId;
target.header.id = path.targetInfo.id;
found = DisplayConfigGetDeviceInfo(&target.header) == ERROR_SUCCESS;
break;
}
}
if (!found) {
return GENERIC_MONITOR_NAME;
}
return QString::fromWCharArray(target.monitorFriendlyDeviceName);
}
#endif
#endif

53
projector.hpp Normal file
View file

@ -0,0 +1,53 @@
#pragma once
#include <obs.hpp>
#include "qt-display.hpp"
#include "vertical-canvas.hpp"
bool IsAlwaysOnTop(QWidget *window);
void SetAlwaysOnTop(QWidget *window, bool enable);
class OBSProjector : public OBSQTDisplay {
Q_OBJECT
private:
CanvasDock *canvas = nullptr;
static void OBSRender(void *data, uint32_t cx, uint32_t cy);
void mousePressEvent(QMouseEvent *event) override;
void closeEvent(QCloseEvent *event) override;
bool isAlwaysOnTop;
bool isAlwaysOnTopOverridden = false;
int savedMonitor = -1;
bool ready = false;
void UpdateProjectorTitle(QString name = "");
QRect prevGeometry;
void SetMonitor(int monitor);
QScreen *screen = nullptr;
private slots:
void EscapeTriggered();
void OpenFullScreenProjector();
void ResizeToContent();
void OpenWindowedProjector();
void AlwaysOnTopToggled(bool alwaysOnTop);
void ScreenRemoved(QScreen *screen_);
public:
OBSProjector(CanvasDock *canvas_, int monitor);
~OBSProjector();
int GetMonitor();
void RenameProjector(QString oldName, QString newName);
void SetHideCursor();
bool IsAlwaysOnTop() const;
bool IsAlwaysOnTopOverridden() const;
void SetIsAlwaysOnTop(bool isAlwaysOnTop, bool isOverridden);
};

260
qt-display.cpp Normal file
View file

@ -0,0 +1,260 @@
#include "qt-display.hpp"
#include "display-helpers.hpp"
#include <QWindow>
#include <QScreen>
#include <QResizeEvent>
#include <QShowEvent>
#include <obs-config.h>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#endif
#if !defined(_WIN32) && !defined(__APPLE__)
#include <obs-nix-platform.h>
#endif
#ifdef ENABLE_WAYLAND
#include <QGuiApplication>
#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
#include <qpa/qplatformnativeinterface.h>
#endif
#endif
class SurfaceEventFilter : public QObject {
OBSQTDisplay *display;
public:
SurfaceEventFilter(OBSQTDisplay *src) : QObject(src), display(src) {}
protected:
bool eventFilter(QObject *obj, QEvent *event) override
{
bool result = QObject::eventFilter(obj, event);
QPlatformSurfaceEvent *surfaceEvent;
switch (event->type()) {
case QEvent::PlatformSurface:
surfaceEvent = static_cast<QPlatformSurfaceEvent *>(event);
switch (surfaceEvent->surfaceEventType()) {
case QPlatformSurfaceEvent::SurfaceCreated:
display->RestoreDisplay();
break;
case QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed:
display->DestroyDisplay();
break;
default:
break;
}
break;
case QEvent::Expose:
display->RestoreDisplay();
break;
default:
break;
}
return result;
}
};
static inline long long color_to_int(const QColor &color)
{
auto shift = [&](unsigned val, int shift_left) {
return ((val & 0xff) << shift_left);
};
return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24);
}
static inline QColor rgba_to_color(uint32_t rgba)
{
return QColor::fromRgb(rgba & 0xFF, (rgba >> 8) & 0xFF, (rgba >> 16) & 0xFF, (rgba >> 24) & 0xFF);
}
OBSQTDisplay::OBSQTDisplay(QWidget *parent, Qt::WindowFlags flags) : QWidget(parent, flags)
{
setAttribute(Qt::WA_PaintOnScreen);
setAttribute(Qt::WA_StaticContents);
setAttribute(Qt::WA_NoSystemBackground);
setAttribute(Qt::WA_OpaquePaintEvent);
setAttribute(Qt::WA_DontCreateNativeAncestors);
setAttribute(Qt::WA_NativeWindow);
auto windowVisible = [this](bool visible) {
if (!visible) {
#if !defined(_WIN32) && !defined(__APPLE__)
display = nullptr;
#endif
return;
}
if (!display) {
CreateDisplay();
} else {
QSize size = GetPixelSize(this);
obs_display_resize(display, size.width(), size.height());
}
};
auto screenChanged = [this](QScreen *) {
CreateDisplay();
QSize size = GetPixelSize(this);
obs_display_resize(display, size.width(), size.height());
};
connect(windowHandle(), &QWindow::visibleChanged, windowVisible);
connect(windowHandle(), &QWindow::screenChanged, screenChanged);
windowHandle()->installEventFilter(new SurfaceEventFilter(this));
}
QColor OBSQTDisplay::GetDisplayBackgroundColor() const
{
return rgba_to_color(backgroundColor);
}
void OBSQTDisplay::SetDisplayBackgroundColor(const QColor &color)
{
uint32_t newBackgroundColor = (uint32_t)color_to_int(color);
if (newBackgroundColor != backgroundColor) {
backgroundColor = newBackgroundColor;
UpdateDisplayBackgroundColor();
}
}
void OBSQTDisplay::UpdateDisplayBackgroundColor()
{
obs_display_set_background_color(display, backgroundColor);
}
static bool QTToGSWindow(QWindow *window, gs_window &gswindow)
{
bool success = true;
#ifdef _WIN32
gswindow.hwnd = (HWND)window->winId();
#elif __APPLE__
gswindow.view = (id)window->winId();
#else
switch (obs_get_nix_platform()) {
case OBS_NIX_PLATFORM_X11_EGL:
gswindow.id = (uint32_t)window->winId();
gswindow.display = obs_get_nix_platform_display();
break;
#ifdef ENABLE_WAYLAND
case OBS_NIX_PLATFORM_WAYLAND: {
#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface();
gswindow.display = native->nativeResourceForWindow("surface", window);
#else
gswindow.display = (void *)window->winId();
#endif
success = gswindow.display != nullptr;
break;
}
#endif
default:
success = false;
break;
}
#endif
return success;
}
void OBSQTDisplay::CreateDisplay()
{
if (display)
return;
if (destroying)
return;
if (!windowHandle()->isExposed())
return;
QSize size = GetPixelSize(this);
gs_init_data info = {};
info.cx = size.width();
info.cy = size.height();
info.format = GS_BGRA;
info.zsformat = GS_ZS_NONE;
if (!QTToGSWindow(windowHandle(), info.window))
return;
display = obs_display_create(&info, backgroundColor);
emit DisplayCreated(this);
}
void OBSQTDisplay::paintEvent(QPaintEvent *event)
{
CreateDisplay();
QWidget::paintEvent(event);
}
void OBSQTDisplay::moveEvent(QMoveEvent *event)
{
QWidget::moveEvent(event);
OnMove();
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
bool OBSQTDisplay::nativeEvent(const QByteArray &, void *message, qintptr *)
#else
bool OBSQTDisplay::nativeEvent(const QByteArray &, void *message, long *)
#endif
{
#ifdef _WIN32
const MSG &msg = *static_cast<MSG *>(message);
switch (msg.message) {
case WM_DISPLAYCHANGE:
OnDisplayChange();
}
#else
UNUSED_PARAMETER(message);
#endif
return false;
}
void OBSQTDisplay::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
CreateDisplay();
if (isVisible() && display) {
QSize size = GetPixelSize(this);
obs_display_resize(display, size.width(), size.height());
}
emit DisplayResized();
}
QPaintEngine *OBSQTDisplay::paintEngine() const
{
return nullptr;
}
void OBSQTDisplay::OnMove()
{
if (display)
obs_display_update_color_space(display);
}
void OBSQTDisplay::OnDisplayChange()
{
if (display)
obs_display_update_color_space(display);
}

56
qt-display.hpp Normal file
View file

@ -0,0 +1,56 @@
#pragma once
#include <QWidget>
#include <obs.hpp>
#define GREY_COLOR_BACKGROUND 0xFF4C4C4C
class OBSQTDisplay : public QWidget {
Q_OBJECT
Q_PROPERTY(
QColor displayBackgroundColor MEMBER backgroundColor READ GetDisplayBackgroundColor WRITE SetDisplayBackgroundColor)
OBSDisplay display;
bool destroying = false;
virtual void paintEvent(QPaintEvent *event) override;
virtual void moveEvent(QMoveEvent *event) override;
virtual void resizeEvent(QResizeEvent *event) override;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override;
#else
virtual bool nativeEvent(const QByteArray &eventType, void *message, long *result) override;
#endif
signals:
void DisplayCreated(OBSQTDisplay *window);
void DisplayResized();
public:
OBSQTDisplay(QWidget *parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags());
~OBSQTDisplay() { display = nullptr; }
virtual QPaintEngine *paintEngine() const override;
inline obs_display_t *GetDisplay() const { return display; }
uint32_t backgroundColor = GREY_COLOR_BACKGROUND;
QColor GetDisplayBackgroundColor() const;
void SetDisplayBackgroundColor(const QColor &color);
void UpdateDisplayBackgroundColor();
void CreateDisplay();
void DestroyDisplay()
{
display = nullptr;
destroying = true;
};
void RestoreDisplay()
{
destroying = false;
CreateDisplay();
};
void OnMove();
void OnDisplayChange();
};

32
resource.rc.in Normal file
View file

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

13
resources.qrc Normal file
View file

@ -0,0 +1,13 @@
<RCC>
<qresource prefix="/aitum">
<file>media/aitum.png</file>
<file>media/record.svg</file>
<file>media/recording.svg</file>
<file>media/stream.svg</file>
<file>media/streaming.svg</file>
<file>media/backtrack_off.svg</file>
<file>media/backtrack_on.svg</file>
<file>media/virtual_cam_off.svg</file>
<file>media/virtual_cam_on.svg</file>
</qresource>
</RCC>

409
scenes-dock.cpp Normal file
View file

@ -0,0 +1,409 @@
#include "scenes-dock.hpp"
#include <QMenu>
#include <QToolBar>
#include <QWidgetAction>
#include "name-dialog.hpp"
#include "obs-module.h"
#include "vertical-canvas.hpp"
void CanvasScenesDock::SetGridMode(bool checked)
{
if (checked) {
sceneList->setResizeMode(QListView::Adjust);
sceneList->setViewMode(QListView::IconMode);
sceneList->setUniformItemSizes(true);
sceneList->setStyleSheet("*{padding: 0; margin: 0;}");
} else {
sceneList->setViewMode(QListView::ListMode);
sceneList->setResizeMode(QListView::Fixed);
sceneList->setStyleSheet("");
}
}
bool CanvasScenesDock::IsGridMode()
{
return sceneList->viewMode() == QListView::IconMode;
}
void CanvasScenesDock::ShowScenesContextMenu(QListWidgetItem *widget_item)
{
auto menu = QMenu(this);
auto a = menu.addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.GridMode")),
[this](bool checked) { SetGridMode(checked); });
a->setCheckable(true);
a->setChecked(IsGridMode());
menu.addAction(QString::fromUtf8(obs_frontend_get_locale_string("Add")), [this] { canvasDock->AddScene(); });
if (!widget_item) {
menu.exec(QCursor::pos());
return;
}
menu.addSeparator();
menu.addAction(QString::fromUtf8(obs_frontend_get_locale_string("Duplicate")), [this] {
auto item = sceneList->currentItem();
if (!item) {
return;
}
canvasDock->AddScene(item->text());
});
menu.addAction(QString::fromUtf8(obs_frontend_get_locale_string("Remove")), [this] {
auto item = sceneList->currentItem();
if (!item) {
return;
}
canvasDock->RemoveScene(item->text());
});
menu.addAction(QString::fromUtf8(obs_frontend_get_locale_string("Rename")), [this] {
const auto item = sceneList->currentItem();
if (!item) {
return;
}
std::string name = item->text().toUtf8().constData();
obs_source_t *source = obs_canvas_get_source_by_name(canvasDock->canvas, name.c_str());
if (!source) {
return;
}
obs_source_t *s = nullptr;
do {
obs_source_release(s);
if (!NameDialog::AskForName(this, QString::fromUtf8(obs_module_text("SceneName")), name)) {
break;
}
s = obs_canvas_get_source_by_name(canvasDock->canvas, name.c_str());
if (s) {
continue;
}
obs_source_set_name(source, name.c_str());
} while (s);
obs_source_release(source);
});
auto orderMenu = menu.addMenu(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Order")));
orderMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Order.MoveUp")),
[this] { ChangeSceneIndex(true, -1, 0); });
orderMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Order.MoveDown")),
[this] { ChangeSceneIndex(true, 1, sceneList->count() - 1); });
orderMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Order.MoveToTop")),
[this] { ChangeSceneIndex(false, 0, 0); });
orderMenu->addAction(QString::fromUtf8(obs_frontend_get_locale_string("Basic.MainMenu.Edit.Order.MoveToBottom")),
[this] { ChangeSceneIndex(false, 1, sceneList->count() - 1); });
menu.addAction(QString::fromUtf8(obs_frontend_get_locale_string("Screenshot.Scene")), [this] {
auto item = sceneList->currentItem();
if (!item) {
return;
}
auto s = obs_canvas_get_source_by_name(canvasDock->canvas, item->text().toUtf8().constData());
if (s) {
obs_frontend_take_source_screenshot(s);
obs_source_release(s);
}
});
menu.addAction(QString::fromUtf8(obs_frontend_get_locale_string("Filters")), [this] {
auto item = sceneList->currentItem();
if (!item) {
return;
}
auto s = obs_canvas_get_source_by_name(canvasDock->canvas, item->text().toUtf8().constData());
if (s) {
obs_frontend_open_source_filters(s);
obs_source_release(s);
}
});
auto tom = menu.addMenu(QString::fromUtf8(obs_frontend_get_locale_string("TransitionOverride")));
std::string scene_name = widget_item->text().toUtf8().constData();
OBSSourceAutoRelease scene_source = obs_canvas_get_source_by_name(canvasDock->canvas, scene_name.c_str());
OBSDataAutoRelease private_settings = obs_source_get_private_settings(scene_source);
obs_data_set_default_int(private_settings, "transition_duration", 300);
const char *curTransition = obs_data_get_string(private_settings, "transition");
int curDuration = (int)obs_data_get_int(private_settings, "transition_duration");
QSpinBox *duration = new QSpinBox(tom);
duration->setMinimum(50);
duration->setSuffix(" ms");
duration->setMaximum(20000);
duration->setSingleStep(50);
duration->setValue(curDuration);
connect(duration, (void (QSpinBox::*)(int))&QSpinBox::valueChanged, [this, scene_name](int dur) {
OBSSourceAutoRelease source = obs_canvas_get_source_by_name(canvasDock->canvas, scene_name.c_str());
OBSDataAutoRelease ps = obs_source_get_private_settings(source);
obs_data_set_int(ps, "transition_duration", dur);
});
auto action = tom->addAction(QString::fromUtf8(obs_frontend_get_locale_string("None")));
action->setCheckable(true);
action->setChecked(!curTransition || !strlen(curTransition));
connect(action, &QAction::triggered, [this, scene_name] {
OBSSourceAutoRelease source = obs_canvas_get_source_by_name(canvasDock->canvas, scene_name.c_str());
OBSDataAutoRelease ps = obs_source_get_private_settings(source);
obs_data_set_string(ps, "transition", "");
});
for (auto t : canvasDock->transitions) {
const char *name = obs_source_get_name(t);
bool match = (name && curTransition && strcmp(name, curTransition) == 0);
if (!name || !*name) {
name = obs_frontend_get_locale_string("None");
}
auto a2 = tom->addAction(QString::fromUtf8(name));
a2->setCheckable(true);
a2->setChecked(match);
connect(a2, &QAction::triggered, [this, scene_name, a2] {
OBSSourceAutoRelease source = obs_canvas_get_source_by_name(canvasDock->canvas, scene_name.c_str());
OBSDataAutoRelease ps = obs_source_get_private_settings(source);
obs_data_set_string(ps, "transition", a2->text().toUtf8().constData());
});
}
QWidgetAction *durationAction = new QWidgetAction(tom);
durationAction->setDefaultWidget(duration);
tom->addSeparator();
tom->addAction(durationAction);
auto linkedScenesMenu = menu.addMenu(QString::fromUtf8(obs_module_text("LinkedScenes")));
connect(linkedScenesMenu, &QMenu::aboutToShow, [linkedScenesMenu, this] {
linkedScenesMenu->clear();
struct obs_frontend_source_list scenes = {};
obs_frontend_get_scenes(&scenes);
for (size_t i = 0; i < scenes.sources.num; i++) {
obs_source_t *src = scenes.sources.array[i];
obs_data_t *settings = obs_source_get_settings(src);
auto name = QString::fromUtf8(obs_source_get_name(src));
auto *checkBox = new QCheckBox(name, linkedScenesMenu);
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
connect(checkBox, &QCheckBox::checkStateChanged, [this, src, checkBox] {
#else
connect(checkBox, &QCheckBox::stateChanged, [this, src, checkBox] {
#endif
canvasDock->SetLinkedScene(src, checkBox->isChecked() ? sceneList->currentItem()->text() : "");
});
auto *checkableAction = new QWidgetAction(linkedScenesMenu);
checkableAction->setDefaultWidget(checkBox);
linkedScenesMenu->addAction(checkableAction);
auto c = obs_data_get_array(settings, "canvas");
if (c) {
const auto count = obs_data_array_count(c);
for (size_t j = 0; j < count; j++) {
auto item = obs_data_array_item(c, j);
if (!item)
continue;
if (obs_data_get_int(item, "width") == canvasDock->canvas_width &&
obs_data_get_int(item, "height") == canvasDock->canvas_height) {
auto sn = QString::fromUtf8(obs_data_get_string(item, "scene"));
if (sn == sceneList->currentItem()->text()) {
checkBox->setChecked(true);
}
}
obs_data_release(item);
}
obs_data_array_release(c);
}
obs_data_release(settings);
}
obs_frontend_source_list_free(&scenes);
});
menu.addAction(QString::fromUtf8(obs_module_text("OnMainCanvas")), [this] {
auto item = sceneList->currentItem();
if (!item) {
return;
}
auto s = obs_canvas_get_source_by_name(canvasDock->canvas, item->text().toUtf8().constData());
if (!s) {
return;
}
if (obs_frontend_preview_program_mode_active()) {
obs_frontend_set_current_preview_scene(s);
} else {
obs_frontend_set_current_scene(s);
}
obs_source_release(s);
});
a = menu.addAction(QString::fromUtf8(obs_frontend_get_locale_string("ShowInMultiview")), [this, scene_name](bool checked) {
OBSSourceAutoRelease source = obs_canvas_get_source_by_name(canvasDock->canvas, scene_name.c_str());
OBSDataAutoRelease ps = obs_source_get_private_settings(source);
obs_data_set_bool(ps, "show_in_multiview", checked);
});
a->setCheckable(true);
obs_data_set_default_bool(private_settings, "show_in_multiview", true);
a->setChecked(obs_data_get_bool(private_settings, "show_in_multiview"));
menu.exec(QCursor::pos());
}
CanvasScenesDock::CanvasScenesDock(CanvasDock *canvas_dock, QWidget *parent) : QFrame(parent), canvasDock(canvas_dock)
{
setMinimumWidth(100);
setMinimumHeight(50);
auto mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
sceneList = new QListWidget();
sceneList->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
sceneList->setFrameShape(QFrame::NoFrame);
sceneList->setFrameShadow(QFrame::Plain);
sceneList->setSelectionMode(QAbstractItemView::SingleSelection);
sceneList->setContextMenuPolicy(Qt::CustomContextMenu);
connect(sceneList, &QListWidget::customContextMenuRequested,
[this](const QPoint &pos) { ShowScenesContextMenu(sceneList->itemAt(pos)); });
connect(sceneList, &QListWidget::currentItemChanged, [this]() {
const auto item = sceneList->currentItem();
if (!item || canvasDock->clearing || canvasDock->switching) {
return;
}
canvasDock->SwitchScene(item->text());
if (!item->isSelected()) {
item->setSelected(true);
}
});
connect(sceneList, &QListWidget::itemSelectionChanged, [this] {
const auto item = sceneList->currentItem();
if (!item) {
return;
}
if (!item->isSelected()) {
item->setSelected(true);
}
});
QAction *renameAction = new QAction(sceneList);
#ifdef __APPLE__
renameAction->setShortcut({Qt::Key_Return});
#else
renameAction->setShortcut({Qt::Key_F2});
#endif
renameAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
connect(renameAction, &QAction::triggered, [this]() {
const auto item = sceneList->currentItem();
if (!item) {
return;
}
obs_source_t *source = obs_canvas_get_source_by_name(canvasDock->canvas, item->text().toUtf8().constData());
if (!source) {
return;
}
std::string name = obs_source_get_name(source);
obs_source_t *s = nullptr;
do {
obs_source_release(s);
if (!NameDialog::AskForName(this, QString::fromUtf8(obs_module_text("SceneName")), name)) {
break;
}
s = obs_canvas_get_source_by_name(canvasDock->canvas, name.c_str());
if (s) {
continue;
}
obs_source_set_name(source, name.c_str());
} while (s);
obs_source_release(source);
});
sceneList->addAction(renameAction);
mainLayout->addWidget(sceneList, 1);
auto toolbar = new QToolBar();
toolbar->setObjectName(QStringLiteral("scenesToolbar"));
toolbar->setIconSize(QSize(16, 16));
toolbar->setFloatable(false);
auto a = toolbar->addAction(QIcon(QString::fromUtf8(":/res/images/plus.svg")),
QString::fromUtf8(obs_frontend_get_locale_string("Add")), [this] { canvasDock->AddScene(); });
toolbar->widgetForAction(a)->setProperty("themeID", QVariant(QString::fromUtf8("addIconSmall")));
toolbar->widgetForAction(a)->setProperty("class", "icon-plus");
a = toolbar->addAction(QIcon(":/res/images/minus.svg"), QString::fromUtf8(obs_frontend_get_locale_string("RemoveScene")),
[this] {
auto item = sceneList->currentItem();
if (!item) {
return;
}
canvasDock->RemoveScene(item->text());
});
toolbar->widgetForAction(a)->setProperty("themeID", QVariant(QString::fromUtf8("removeIconSmall")));
toolbar->widgetForAction(a)->setProperty("class", "icon-minus");
a->setShortcutContext(Qt::WidgetWithChildrenShortcut);
#ifdef __APPLE__
a->setShortcut({Qt::Key_Backspace});
#else
a->setShortcut({Qt::Key_Delete});
#endif
sceneList->addAction(a);
toolbar->addSeparator();
a = toolbar->addAction(
QIcon(":/res/images/filter.svg"), QString::fromUtf8(obs_frontend_get_locale_string("SceneFilters")), [this] {
auto item = sceneList->currentItem();
if (!item) {
return;
}
auto s = obs_canvas_get_source_by_name(canvasDock->canvas, item->text().toUtf8().constData());
if (!s) {
return;
}
obs_frontend_open_source_filters(s);
obs_source_release(s);
});
toolbar->widgetForAction(a)->setProperty("themeID", QVariant(QString::fromUtf8("filtersIcon")));
toolbar->widgetForAction(a)->setProperty("class", "icon-filter");
toolbar->addSeparator();
a = toolbar->addAction(QIcon(":/res/images/up.svg"), QString::fromUtf8(obs_frontend_get_locale_string("MoveSceneUp")),
[this] { ChangeSceneIndex(true, -1, 0); });
toolbar->widgetForAction(a)->setProperty("themeID", QVariant(QString::fromUtf8("upArrowIconSmall")));
toolbar->widgetForAction(a)->setProperty("class", "icon-up");
a = toolbar->addAction(QIcon(":/res/images/down.svg"), QString::fromUtf8(obs_frontend_get_locale_string("MoveSceneDown")),
[this] { ChangeSceneIndex(true, 1, sceneList->count() - 1); });
toolbar->widgetForAction(a)->setProperty("themeID", QVariant(QString::fromUtf8("downArrowIconSmall")));
toolbar->widgetForAction(a)->setProperty("class", "icon-down");
mainLayout->addWidget(toolbar, 0);
setObjectName(QStringLiteral("contextContainer"));
setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
mainLayout->setContentsMargins(0, 0, 0, 0);
setLayout(mainLayout);
}
void CanvasScenesDock::ChangeSceneIndex(bool relative, int offset, int invalidIdx)
{
int idx = sceneList->currentRow();
if (idx < 0) {
return;
}
auto canvasItem = sceneList->item(idx);
if (!canvasItem) {
return;
}
if (idx == invalidIdx) {
return;
}
sceneList->blockSignals(true);
auto item = sceneList->takeItem(idx);
if (relative) {
sceneList->insertItem(idx + offset, item);
sceneList->setCurrentRow(idx + offset);
} else if (offset == 0) {
sceneList->insertItem(offset, item);
} else {
sceneList->insertItem(sceneList->count(), item);
}
item->setSelected(true);
sceneList->blockSignals(false);
}
CanvasScenesDock::~CanvasScenesDock() {}

25
scenes-dock.hpp Normal file
View file

@ -0,0 +1,25 @@
#pragma once
#include <QDockWidget>
#include <QListWidget>
#include <QWidget>
class CanvasDock;
class CanvasScenesDock : public QFrame {
Q_OBJECT
friend class CanvasDock;
private:
QListWidget *sceneList;
CanvasDock *canvasDock;
void ChangeSceneIndex(bool relative, int offset, int invalidIdx);
void ShowScenesContextMenu(QListWidgetItem *item);
void SetGridMode(bool checked);
bool IsGridMode();
public:
CanvasScenesDock(CanvasDock *canvas_dock, QWidget *parent = nullptr);
~CanvasScenesDock();
};

1662
source-tree.cpp Normal file

File diff suppressed because it is too large Load diff

225
source-tree.hpp Normal file
View file

@ -0,0 +1,225 @@
#pragma once
#include <QList>
#include <QVector>
#include <QPointer>
#include <QListView>
#include <QCheckBox>
#include <QStaticText>
#include <QAbstractListModel>
#include <QStyledItemDelegate>
#include <obs.hpp>
#include <obs-frontend-api.h>
#include "vertical-canvas.hpp"
class QLabel;
class QCheckBox;
class QLineEdit;
class SourceTree;
class QSpacerItem;
class QHBoxLayout;
class LockedCheckBox;
class VisibilityCheckBox;
class VisibilityItemWidget;
class SourceTreeSubItemCheckBox : public QCheckBox {
Q_OBJECT
};
class SourceTreeItem : public QFrame {
Q_OBJECT
friend class SourceTree;
friend class SourceTreeModel;
void mouseDoubleClickEvent(QMouseEvent *event) override;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void enterEvent(QEnterEvent *event) override;
#else
void enterEvent(QEvent *event) override;
#endif
void leaveEvent(QEvent *event) override;
virtual bool eventFilter(QObject *object, QEvent *event) override;
void Update(bool force);
enum class Type {
Unknown,
Item,
Group,
SubItem,
};
void DisconnectSignals();
void ReconnectSignals();
Type type = Type::Unknown;
public:
explicit SourceTreeItem(SourceTree *tree, OBSSceneItem sceneitem);
~SourceTreeItem();
bool IsEditing();
private:
QSpacerItem *spacer = nullptr;
QCheckBox *expand = nullptr;
QLabel *iconLabel = nullptr;
VisibilityCheckBox *vis = nullptr;
LockedCheckBox *lock = nullptr;
QHBoxLayout *boxLayout = nullptr;
QLabel *label = nullptr;
QLineEdit *editor = nullptr;
std::string newName;
SourceTree *tree;
OBSSceneItem sceneitem;
std::vector<OBSSignal> sigs;
static void removeScene(void *data, calldata_t *cd);
static void removeItem(void *data, calldata_t *cd);
static void itemVisible(void *data, calldata_t *cd);
static void itemLocked(void *data, calldata_t *cd);
static void itemSelect(void *data, calldata_t *cd);
static void itemDeselect(void *data, calldata_t *cd);
static void reorderGroup(void *data, calldata_t *);
static void renamed(void *data, calldata_t *cd);
static void removeSource(void *data, calldata_t *);
virtual void paintEvent(QPaintEvent *event) override;
void ExitEditModeInternal(bool save);
private slots:
void Clear();
void EnterEditMode();
void ExitEditMode(bool save);
void VisibilityChanged(bool visible);
void LockedChanged(bool locked);
void Renamed(const QString &name);
void ExpandClicked(bool checked);
void Select();
void Deselect();
};
class SourceTreeModel : public QAbstractListModel {
Q_OBJECT
friend class SourceTree;
friend class SourceTreeItem;
friend class CanvasDock;
SourceTree *st;
QVector<OBSSceneItem> items;
bool hasGroups = false;
static void OBSFrontendEvent(enum obs_frontend_event event, void *ptr);
void Clear();
void SceneChanged();
void ReorderItems();
void Add(obs_sceneitem_t *item);
void Remove(obs_sceneitem_t *item);
OBSSceneItem Get(int idx);
QString GetNewGroupName();
void AddGroup();
void GroupSelectedItems(QModelIndexList &indices);
void UngroupSelectedGroups(QModelIndexList &indices);
void ExpandGroup(obs_sceneitem_t *item);
void CollapseGroup(obs_sceneitem_t *item);
void UpdateGroupState(bool update);
public:
explicit SourceTreeModel(SourceTree *st);
~SourceTreeModel();
virtual int rowCount(const QModelIndex &parent) const override;
virtual QVariant data(const QModelIndex &index, int role) const override;
virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
virtual Qt::DropActions supportedDropActions() const override;
};
class SourceTree : public QListView {
Q_OBJECT
bool ignoreReorder = false;
CanvasDock *canvasDock;
friend class SourceTreeModel;
friend class SourceTreeItem;
friend class CanvasDock;
bool textPrepared = false;
QStaticText textNoSources;
//QSvgRenderer iconNoSources;
bool iconsVisible = true;
void UpdateNoSourcesMessage();
void ResetWidgets();
void UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item);
void UpdateWidgets(bool force = false);
inline SourceTreeModel *GetStm() const { return reinterpret_cast<SourceTreeModel *>(model()); }
public:
inline SourceTreeItem *GetItemWidget(int idx)
{
QWidget *widget = indexWidget(GetStm()->createIndex(idx, 0));
return reinterpret_cast<SourceTreeItem *>(widget);
}
explicit SourceTree(CanvasDock *canvas_dock, QWidget *parent = nullptr);
inline bool IgnoreReorder() const { return ignoreReorder; }
inline void Clear() { GetStm()->Clear(); }
inline void Add(obs_sceneitem_t *item) { GetStm()->Add(item); }
inline OBSSceneItem Get(int idx) { return GetStm()->Get(idx); }
inline QString GetNewGroupName() { return GetStm()->GetNewGroupName(); }
void SelectItem(obs_sceneitem_t *sceneitem, bool select);
bool MultipleBaseSelected() const;
bool GroupsSelected() const;
bool GroupedItemsSelected() const;
void UpdateIcons();
void SetIconsVisible(bool visible);
public slots:
inline void ReorderItems() { GetStm()->ReorderItems(); }
inline void RefreshItems() { GetStm()->SceneChanged(); }
void Remove(OBSSceneItem item, OBSScene scene);
void GroupSelectedItems();
void UngroupSelectedGroups();
void AddGroup();
bool Edit(int idx);
void NewGroupEdit(int idx);
protected:
virtual void mouseDoubleClickEvent(QMouseEvent *event) override;
virtual void dropEvent(QDropEvent *event) override;
virtual void paintEvent(QPaintEvent *event) override;
virtual void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override;
};
class SourceTreeDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
SourceTreeDelegate(QObject *parent);
virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};

227
sources-dock.cpp Normal file
View file

@ -0,0 +1,227 @@
#include "sources-dock.hpp"
#include <QMenu>
#include <QMessageBox>
#include <QToolBar>
#include <QVBoxLayout>
#include "obs-module.h"
#include "vertical-canvas.hpp"
#include "name-dialog.hpp"
CanvasSourcesDock::CanvasSourcesDock(CanvasDock *canvas_dock, QWidget *parent) : QFrame(parent), canvasDock(canvas_dock)
{
setMinimumWidth(100);
setMinimumHeight(50);
auto mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
sourceList = new SourceTree(canvas_dock, this);
sourceList->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
sourceList->setFrameShape(QFrame::NoFrame);
sourceList->setFrameShadow(QFrame::Plain);
sourceList->setSelectionMode(QAbstractItemView::ExtendedSelection);
sourceList->setContextMenuPolicy(Qt::CustomContextMenu);
sourceList->setDropIndicatorShown(true);
sourceList->setDragEnabled(true);
sourceList->setDragDropMode(QAbstractItemView::InternalMove);
sourceList->setDefaultDropAction(Qt::TargetMoveAction);
connect(sourceList, &SourceTree::customContextMenuRequested, [this] { ShowSourcesContextMenu(GetCurrentSceneItem()); });
QAction *renameAction = new QAction(sourceList);
#ifdef __APPLE__
renameAction->setShortcut({Qt::Key_Return});
#else
renameAction->setShortcut({Qt::Key_F2});
#endif
renameAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
connect(renameAction, &QAction::triggered, [this]() {
obs_sceneitem_t *sceneItem = GetCurrentSceneItem();
if (!sceneItem)
return;
obs_source_t *source = obs_source_get_ref(obs_sceneitem_get_source(sceneItem));
if (!source)
return;
std::string name = obs_source_get_name(source);
obs_source_t *s = nullptr;
do {
obs_source_release(s);
if (!NameDialog::AskForName(this, QString::fromUtf8(obs_module_text("SourceName")), name)) {
break;
}
s = obs_canvas_get_source_by_name(canvasDock->canvas, name.c_str());
if (s)
continue;
obs_source_set_name(source, name.c_str());
} while (s);
obs_source_release(source);
});
sourceList->addAction(renameAction);
mainLayout->addWidget(sourceList, 1);
auto toolbar = new QToolBar();
toolbar->setObjectName(QStringLiteral("scenesToolbar"));
toolbar->setIconSize(QSize(16, 16));
toolbar->setFloatable(false);
auto a = toolbar->addAction(QIcon(QString::fromUtf8(":/res/images/plus.svg")),
QString::fromUtf8(obs_frontend_get_locale_string("AddSource")), [this] {
const auto menu = canvasDock->CreateAddSourcePopupMenu();
menu->exec(QCursor::pos());
});
toolbar->widgetForAction(a)->setProperty("themeID", QVariant(QString::fromUtf8("addIconSmall")));
toolbar->widgetForAction(a)->setProperty("class", "icon-plus");
a = toolbar->addAction(
QIcon(":/res/images/minus.svg"), QString::fromUtf8(obs_frontend_get_locale_string("RemoveSource")), [this] {
std::vector<OBSSceneItem> items;
obs_scene_enum_items(canvasDock->scene, selected_items, &items);
if (!items.size())
return;
/* ------------------------------------- */
/* confirm action with user */
bool confirmed = false;
if (items.size() > 1) {
QString text = QString::fromUtf8(obs_frontend_get_locale_string("ConfirmRemove.TextMultiple"))
.arg(QString::number(items.size()));
QMessageBox remove_items(this);
remove_items.setText(text);
QPushButton *Yes = remove_items.addButton(QString::fromUtf8(obs_frontend_get_locale_string("Yes")),
QMessageBox::YesRole);
remove_items.setDefaultButton(Yes);
remove_items.addButton(QString::fromUtf8(obs_frontend_get_locale_string("No")),
QMessageBox::NoRole);
remove_items.setIcon(QMessageBox::Question);
remove_items.setWindowTitle(
QString::fromUtf8(obs_frontend_get_locale_string("ConfirmRemove.Title")));
remove_items.exec();
confirmed = Yes == remove_items.clickedButton();
} else {
OBSSceneItem &item = items[0];
obs_source_t *source = obs_sceneitem_get_source(item);
if (source) {
const char *name = obs_source_get_name(source);
QString text = QString::fromUtf8(obs_frontend_get_locale_string("ConfirmRemove.Text"))
.arg(QString::fromUtf8(name));
QMessageBox remove_source(this);
remove_source.setText(text);
QPushButton *Yes = remove_source.addButton(
QString::fromUtf8(obs_frontend_get_locale_string("Yes")), QMessageBox::YesRole);
remove_source.setDefaultButton(Yes);
remove_source.addButton(QString::fromUtf8(obs_frontend_get_locale_string("No")),
QMessageBox::NoRole);
remove_source.setIcon(QMessageBox::Question);
remove_source.setWindowTitle(
QString::fromUtf8(obs_frontend_get_locale_string("ConfirmRemove.Title")));
remove_source.exec();
confirmed = Yes == remove_source.clickedButton();
}
}
if (!confirmed)
return;
/* ----------------------------------------------- */
/* remove items */
for (auto &item : items)
obs_sceneitem_remove(item);
});
toolbar->widgetForAction(a)->setProperty("themeID", QVariant(QString::fromUtf8("removeIconSmall")));
toolbar->widgetForAction(a)->setProperty("class", "icon-minus");
a->setShortcutContext(Qt::WidgetWithChildrenShortcut);
#ifdef __APPLE__
a->setShortcut({Qt::Key_Backspace});
#else
a->setShortcut({Qt::Key_Delete});
#endif
sourceList->addAction(a);
toolbar->addSeparator();
a = toolbar->addAction(QIcon(":/res/images/filter.svg"), QString::fromUtf8(obs_frontend_get_locale_string("SourceFilters")),
[this] {
auto item = GetCurrentSceneItem();
auto source = obs_sceneitem_get_source(item);
if (source)
obs_frontend_open_source_filters(source);
});
toolbar->widgetForAction(a)->setProperty("themeID", QVariant(QString::fromUtf8("filtersIcon")));
toolbar->widgetForAction(a)->setProperty("class", "icon-filter");
a = toolbar->addAction(QIcon(":/settings/images/settings/general.svg"),
QString::fromUtf8(obs_frontend_get_locale_string("SourceProperties")), [this] {
auto item = GetCurrentSceneItem();
auto source = obs_sceneitem_get_source(item);
if (source)
obs_frontend_open_source_properties(source);
});
toolbar->widgetForAction(a)->setProperty("themeID", QVariant(QString::fromUtf8("propertiesIconSmall")));
toolbar->widgetForAction(a)->setProperty("class", "icon-gear");
toolbar->addSeparator();
a = toolbar->addAction(QIcon(":/res/images/up.svg"), QString::fromUtf8(obs_frontend_get_locale_string("MoveSourceUp")),
[this] {
auto item = GetCurrentSceneItem();
obs_sceneitem_set_order(item, OBS_ORDER_MOVE_UP);
});
toolbar->widgetForAction(a)->setProperty("themeID", QVariant(QString::fromUtf8("upArrowIconSmall")));
toolbar->widgetForAction(a)->setProperty("class", "icon-up");
a = toolbar->addAction(QIcon(":/res/images/down.svg"), QString::fromUtf8(obs_frontend_get_locale_string("MoveSourceDown")),
[this] {
auto item = GetCurrentSceneItem();
obs_sceneitem_set_order(item, OBS_ORDER_MOVE_DOWN);
});
toolbar->widgetForAction(a)->setProperty("themeID", QVariant(QString::fromUtf8("downArrowIconSmall")));
toolbar->widgetForAction(a)->setProperty("class", "icon-down");
mainLayout->addWidget(toolbar, 0);
setObjectName(QStringLiteral("contextContainer"));
setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
mainLayout->setContentsMargins(0, 0, 0, 0);
setLayout(mainLayout);
}
CanvasSourcesDock::~CanvasSourcesDock() {}
void CanvasSourcesDock::ShowSourcesContextMenu(obs_sceneitem_t *item)
{
auto menu = QMenu(this);
menu.addMenu(canvasDock->CreateAddSourcePopupMenu());
if (item) {
canvasDock->AddSceneItemMenuItems(&menu, item);
}
menu.exec(QCursor::pos());
}
bool CanvasSourcesDock::selected_items(obs_scene_t *, obs_sceneitem_t *item, void *param)
{
std::vector<OBSSceneItem> &items = *reinterpret_cast<std::vector<OBSSceneItem> *>(param);
if (obs_sceneitem_selected(item)) {
items.emplace_back(item);
} else if (obs_sceneitem_is_group(item)) {
obs_sceneitem_group_enum_items(item, selected_items, &items);
}
return true;
}
obs_sceneitem_t *CanvasSourcesDock::GetCurrentSceneItem()
{
return sourceList->Get(GetTopSelectedSourceItem());
}
int CanvasSourcesDock::GetTopSelectedSourceItem()
{
QModelIndexList selectedItems = sourceList->selectionModel()->selectedIndexes();
return selectedItems.count() ? selectedItems[0].row() : -1;
}

28
sources-dock.hpp Normal file
View file

@ -0,0 +1,28 @@
#pragma once
#include <QDockWidget>
#include <QListWidget>
#include <QWidget>
#include "source-tree.hpp"
class CanvasDock;
class SourceTree;
class CanvasSourcesDock : public QFrame {
Q_OBJECT
friend class CanvasDock;
private:
SourceTree *sourceList;
CanvasDock *canvasDock;
obs_sceneitem_t *GetCurrentSceneItem();
int GetTopSelectedSourceItem();
void ShowSourcesContextMenu(obs_sceneitem_t *item);
static bool selected_items(obs_scene_t *, obs_sceneitem_t *item, void *param);
public:
CanvasSourcesDock(CanvasDock *canvas_dock, QWidget *parent = nullptr);
~CanvasSourcesDock();
};

243
transitions-dock.cpp Normal file
View file

@ -0,0 +1,243 @@
#include "transitions-dock.hpp"
#include "obs-module.h"
#include "vertical-canvas.hpp"
#include <QMenu>
#include "name-dialog.hpp"
#include <QMessageBox>
CanvasTransitionsDock::CanvasTransitionsDock(CanvasDock *canvas_dock, QWidget *parent) : QFrame(parent), canvasDock(canvas_dock)
{
setMinimumWidth(100);
setMinimumHeight(50);
setContentsMargins(0, 0, 0, 0);
auto mainLayout = new QVBoxLayout();
mainLayout->setContentsMargins(4, 4, 4, 4);
mainLayout->setSpacing(2);
transition = new QComboBox();
mainLayout->addWidget(transition);
auto hl = new QHBoxLayout();
/* auto l = new QLabel(QString::fromUtf8(
obs_frontend_get_locale_string("Basic.TransitionDuration")));
l->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
hl->addWidget(l);
duration = new QSpinBox();
duration->setSuffix(" ms");
duration->setMinimum(50);
duration->setMaximum(20000);
duration->setSingleStep(50);
duration->setValue(300);
l->setBuddy(duration);
hl->addWidget(duration);
mainLayout->addLayout(hl);
hl = new QHBoxLayout();*/
hl->addStretch();
auto addButton = new QPushButton();
addButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
addButton->setAccessibleName(QString::fromUtf8(obs_frontend_get_locale_string("Basic.AddTransition")));
addButton->setToolTip(QString::fromUtf8(obs_frontend_get_locale_string("Basic.AddTransition")));
addButton->setIcon(QIcon(":/res/images/add.png"));
addButton->setProperty("themeID", "addIconSmall");
addButton->setProperty("class", "icon-plus");
addButton->setProperty("toolButton", true);
addButton->setFlat(false);
connect(addButton, &QPushButton::clicked, [this] {
auto menu = QMenu(this);
auto subMenu = menu.addMenu(QString::fromUtf8(obs_module_text("CopyFromMain")));
struct obs_frontend_source_list transitions = {};
obs_frontend_get_transitions(&transitions);
for (size_t i = 0; i < transitions.sources.num; i++) {
auto tr = transitions.sources.array[i];
const char *name = obs_source_get_name(tr);
auto action = subMenu->addAction(QString::fromUtf8(name));
if (!obs_is_source_configurable(obs_source_get_unversioned_id(tr))) {
action->setEnabled(false);
}
for (auto t : canvasDock->transitions) {
if (strcmp(name, obs_source_get_name(t)) == 0) {
action->setEnabled(false);
break;
}
}
connect(action, &QAction::triggered, [this, tr] {
OBSDataAutoRelease d = obs_save_source(tr);
OBSSourceAutoRelease t = obs_load_private_source(d);
if (t) {
canvasDock->transitions.emplace_back(t);
auto n = QString::fromUtf8(obs_source_get_name(t));
transition->addItem(n);
transition->setCurrentText(n);
}
});
}
obs_frontend_source_list_free(&transitions);
menu.addSeparator();
size_t idx = 0;
const char *id;
while (obs_enum_transition_types(idx++, &id)) {
if (!obs_is_source_configurable(id))
continue;
const char *display_name = obs_source_get_display_name(id);
auto action = menu.addAction(QString::fromUtf8(display_name));
connect(action, &QAction::triggered, [this, id] {
OBSSourceAutoRelease t = obs_source_create_private(id, obs_source_get_display_name(id), nullptr);
if (t) {
std::string name = obs_source_get_name(t);
while (true) {
if (!NameDialog::AskForName(
this, QString::fromUtf8(obs_module_text("TransitionName")), name)) {
obs_source_release(t);
return;
}
if (name.empty())
continue;
bool found = false;
for (auto tr : canvasDock->transitions) {
if (strcmp(obs_source_get_name(tr), name.c_str()) == 0) {
found = true;
break;
}
}
if (found)
continue;
obs_source_set_name(t, name.c_str());
break;
}
canvasDock->transitions.emplace_back(t);
auto n = QString::fromUtf8(obs_source_get_name(t));
transition->addItem(n);
transition->setCurrentText(n);
obs_frontend_open_source_properties(t);
}
});
}
menu.exec(QCursor::pos());
});
hl->addWidget(addButton);
removeButton = new QPushButton();
removeButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
removeButton->setAccessibleName(QString::fromUtf8(obs_frontend_get_locale_string("Basic.RemoveTransition")));
removeButton->setToolTip(QString::fromUtf8(obs_frontend_get_locale_string("Basic.RemoveTransition")));
removeButton->setIcon(QIcon(":/res/images/list_remove.png"));
removeButton->setProperty("themeID", "removeIconSmall");
removeButton->setProperty("class", "icon-minus");
removeButton->setProperty("toolButton", true);
removeButton->setFlat(false);
connect(removeButton, &QPushButton::clicked, [this] {
QMessageBox mb(
QMessageBox::Question, QString::fromUtf8(obs_frontend_get_locale_string("ConfirmRemove.Title")),
QString::fromUtf8(obs_frontend_get_locale_string("ConfirmRemove.Text")).arg(transition->currentText()),
QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No));
mb.setDefaultButton(QMessageBox::NoButton);
if (mb.exec() != QMessageBox::Yes)
return;
auto n = transition->currentText().toUtf8();
for (auto it = canvasDock->transitions.begin(); it != canvasDock->transitions.end(); ++it) {
if (strcmp(n.constData(), obs_source_get_name(it->Get())) == 0) {
if (!obs_is_source_configurable(obs_source_get_unversioned_id(it->Get())))
return;
canvasDock->transitions.erase(it);
break;
}
}
transition->removeItem(transition->currentIndex());
if (transition->currentIndex() < 0)
transition->setCurrentIndex(0);
});
hl->addWidget(removeButton);
propsButton = new QPushButton();
propsButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
propsButton->setAccessibleName(QString::fromUtf8(obs_frontend_get_locale_string("Basic.TransitionProperties")));
propsButton->setToolTip(QString::fromUtf8(obs_frontend_get_locale_string("Basic.TransitionProperties")));
propsButton->setIcon(QIcon(":/settings/images/settings/general.svg"));
propsButton->setProperty("themeID", "menuIconSmall");
propsButton->setProperty("class", "icon-dots-vert");
propsButton->setProperty("toolButton", true);
propsButton->setFlat(false);
connect(propsButton, &QPushButton::clicked, [this] {
auto menu = QMenu(this);
auto action = menu.addAction(QString::fromUtf8(obs_frontend_get_locale_string("Rename")));
connect(action, &QAction::triggered, [this] {
auto tn = transition->currentText().toUtf8();
obs_source_t *t = canvasDock->GetTransition(tn.constData());
if (!t)
return;
std::string name = obs_source_get_name(t);
while (true) {
if (!NameDialog::AskForName(this, QString::fromUtf8(obs_module_text("TransitionName")), name)) {
return;
}
if (name.empty())
continue;
bool found = false;
for (auto tr : canvasDock->transitions) {
if (strcmp(obs_source_get_name(tr), name.c_str()) == 0) {
found = true;
break;
}
}
if (found)
continue;
transition->setItemText(transition->currentIndex(), QString::fromUtf8(name.c_str()));
obs_source_set_name(t, name.c_str());
break;
}
});
action = menu.addAction(QString::fromUtf8(obs_frontend_get_locale_string("Properties")));
connect(action, &QAction::triggered, [this] {
auto tn = transition->currentText().toUtf8();
auto t = canvasDock->GetTransition(tn.constData());
if (!t)
return;
obs_frontend_open_source_properties(t);
});
menu.exec(QCursor::pos());
});
hl->addWidget(propsButton);
mainLayout->addLayout(hl);
mainLayout->addStretch();
setObjectName(QStringLiteral("contextContainer"));
setLayout(mainLayout);
for (auto t : canvasDock->transitions) {
auto name = QString::fromUtf8(obs_source_get_name(t));
transition->addItem(name);
if (obs_weak_source_references_source(canvasDock->source, t)) {
transition->setCurrentText(name);
bool config = obs_is_source_configurable(obs_source_get_unversioned_id(t));
removeButton->setEnabled(config);
propsButton->setEnabled(config);
}
}
connect(transition, &QComboBox::currentTextChanged, [this] {
auto tn = transition->currentText().toUtf8();
auto t = canvasDock->GetTransition(tn.constData());
if (!t)
return;
canvasDock->SwapTransition(t);
bool config = obs_is_source_configurable(obs_source_get_unversioned_id(t));
removeButton->setEnabled(config);
propsButton->setEnabled(config);
});
}
CanvasTransitionsDock::~CanvasTransitionsDock() {}

25
transitions-dock.hpp Normal file
View file

@ -0,0 +1,25 @@
#pragma once
#include <QDockWidget>
#include <QComboBox>
#include <QSpinBox>
#include <QPushButton>
#include <QWidget>
class CanvasDock;
class CanvasTransitionsDock : public QFrame {
Q_OBJECT
friend class CanvasDock;
private:
CanvasDock *canvasDock;
QComboBox *transition;
QSpinBox *duration;
QPushButton *removeButton;
QPushButton *propsButton;
public:
CanvasTransitionsDock(CanvasDock *canvas_dock, QWidget *parent = nullptr);
~CanvasTransitionsDock();
};

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