mirror of
https://github.com/obsproject/obs-studio.git
synced 2026-09-24 23:31:36 +08:00
frontend: Migrate frontend plugins
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
add_subdirectory(aja-output-ui)
|
||||
add_subdirectory(decklink-captions)
|
||||
add_subdirectory(decklink-output-ui)
|
||||
add_subdirectory(frontend-tools)
|
||||
@@ -0,0 +1,247 @@
|
||||
#include "AJAOutputUI.h"
|
||||
#include "aja-ui-main.h"
|
||||
|
||||
#include "../../../plugins/aja/aja-common.hpp"
|
||||
#include "../../../plugins/aja/aja-ui-props.hpp"
|
||||
#include "../../../plugins/aja/aja-enums.hpp"
|
||||
#include "../../../plugins/aja/aja-card-manager.hpp"
|
||||
|
||||
#include <ajantv2/includes/ntv2card.h>
|
||||
#include <ajantv2/includes/ntv2devicefeatures.h>
|
||||
#include <ajantv2/includes/ntv2enums.h>
|
||||
#include <ajantv2/includes/ntv2utils.h>
|
||||
|
||||
#include <obs-module.h>
|
||||
#include <util/platform.h>
|
||||
#include <util/util.hpp>
|
||||
|
||||
AJAOutputUI::AJAOutputUI(QWidget *parent) : QDialog(parent), ui(new Ui_Output)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
setSizeGripEnabled(true);
|
||||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||
|
||||
propertiesView = nullptr;
|
||||
previewPropertiesView = nullptr;
|
||||
miscPropertiesView = nullptr;
|
||||
}
|
||||
|
||||
void AJAOutputUI::ShowHideDialog()
|
||||
{
|
||||
SetupPropertiesView();
|
||||
SetupPreviewPropertiesView();
|
||||
SetupMiscPropertiesView();
|
||||
|
||||
setVisible(!isVisible());
|
||||
}
|
||||
|
||||
void AJAOutputUI::SetupPropertiesView()
|
||||
{
|
||||
if (propertiesView)
|
||||
delete propertiesView;
|
||||
|
||||
obs_data_t *settings = obs_data_create();
|
||||
OBSData data = load_settings(kProgramPropsFilename);
|
||||
if (data) {
|
||||
obs_data_apply(settings, data);
|
||||
} else {
|
||||
// apply default settings
|
||||
obs_data_set_default_int(settings, kUIPropOutput.id, static_cast<long long>(IOSelection::Invalid));
|
||||
obs_data_set_default_int(settings, kUIPropVideoFormatSelect.id,
|
||||
static_cast<long long>(kDefaultAJAVideoFormat));
|
||||
obs_data_set_default_int(settings, kUIPropPixelFormatSelect.id,
|
||||
static_cast<long long>(kDefaultAJAPixelFormat));
|
||||
obs_data_set_default_int(settings, kUIPropSDITransport.id,
|
||||
static_cast<long long>(kDefaultAJASDITransport));
|
||||
obs_data_set_default_int(settings, kUIPropSDITransport4K.id,
|
||||
static_cast<long long>(kDefaultAJASDITransport4K));
|
||||
}
|
||||
|
||||
// Assign an ID to the program output plugin instance for channel usage tracking
|
||||
obs_data_set_string(settings, kUIPropAJAOutputID.id, kProgramOutputID);
|
||||
|
||||
propertiesView =
|
||||
new OBSPropertiesView(settings, "aja_output", (PropertiesReloadCallback)obs_get_output_properties, 170);
|
||||
|
||||
ui->propertiesLayout->addWidget(propertiesView);
|
||||
obs_data_release(settings);
|
||||
|
||||
connect(propertiesView, &OBSPropertiesView::Changed, this, &AJAOutputUI::PropertiesChanged);
|
||||
}
|
||||
|
||||
void AJAOutputUI::SaveSettings(const char *filename, obs_data_t *settings)
|
||||
{
|
||||
BPtr<char> modulePath = obs_module_get_config_path(obs_current_module(), "");
|
||||
|
||||
os_mkdirs(modulePath);
|
||||
|
||||
BPtr<char> path = obs_module_get_config_path(obs_current_module(), filename);
|
||||
|
||||
if (settings)
|
||||
obs_data_save_json_safe(settings, path, "tmp", "bak");
|
||||
}
|
||||
|
||||
void AJAOutputUI::SetupPreviewPropertiesView()
|
||||
{
|
||||
if (previewPropertiesView)
|
||||
delete previewPropertiesView;
|
||||
|
||||
obs_data_t *settings = obs_data_create();
|
||||
|
||||
OBSData data = load_settings(kPreviewPropsFilename);
|
||||
if (data) {
|
||||
obs_data_apply(settings, data);
|
||||
} else {
|
||||
// apply default settings
|
||||
obs_data_set_default_int(settings, kUIPropOutput.id, static_cast<long long>(IOSelection::Invalid));
|
||||
obs_data_set_default_int(settings, kUIPropVideoFormatSelect.id,
|
||||
static_cast<long long>(kDefaultAJAVideoFormat));
|
||||
obs_data_set_default_int(settings, kUIPropPixelFormatSelect.id,
|
||||
static_cast<long long>(kDefaultAJAPixelFormat));
|
||||
obs_data_set_default_int(settings, kUIPropSDITransport.id,
|
||||
static_cast<long long>(kDefaultAJASDITransport));
|
||||
obs_data_set_default_int(settings, kUIPropSDITransport4K.id,
|
||||
static_cast<long long>(kDefaultAJASDITransport4K));
|
||||
}
|
||||
|
||||
// Assign an ID to the program output plugin instance for channel usage tracking
|
||||
obs_data_set_string(settings, kUIPropAJAOutputID.id, kPreviewOutputID);
|
||||
|
||||
previewPropertiesView =
|
||||
new OBSPropertiesView(settings, "aja_output", (PropertiesReloadCallback)obs_get_output_properties, 170);
|
||||
|
||||
ui->previewPropertiesLayout->addWidget(previewPropertiesView);
|
||||
obs_data_release(settings);
|
||||
|
||||
connect(previewPropertiesView, &OBSPropertiesView::Changed, this, &AJAOutputUI::PreviewPropertiesChanged);
|
||||
}
|
||||
|
||||
void AJAOutputUI::on_outputButton_clicked()
|
||||
{
|
||||
SaveSettings(kProgramPropsFilename, propertiesView->GetSettings());
|
||||
output_toggle();
|
||||
}
|
||||
|
||||
void AJAOutputUI::PropertiesChanged()
|
||||
{
|
||||
SaveSettings(kProgramPropsFilename, propertiesView->GetSettings());
|
||||
}
|
||||
|
||||
void AJAOutputUI::OutputStateChanged(bool active)
|
||||
{
|
||||
QString text;
|
||||
if (active) {
|
||||
text = QString(obs_module_text("Stop"));
|
||||
} else {
|
||||
text = QString(obs_module_text("Start"));
|
||||
}
|
||||
|
||||
ui->outputButton->setChecked(active);
|
||||
ui->outputButton->setText(text);
|
||||
}
|
||||
|
||||
void AJAOutputUI::on_previewOutputButton_clicked()
|
||||
{
|
||||
SaveSettings(kPreviewPropsFilename, previewPropertiesView->GetSettings());
|
||||
preview_output_toggle();
|
||||
}
|
||||
|
||||
void AJAOutputUI::PreviewPropertiesChanged()
|
||||
{
|
||||
SaveSettings(kPreviewPropsFilename, previewPropertiesView->GetSettings());
|
||||
}
|
||||
|
||||
void AJAOutputUI::PreviewOutputStateChanged(bool active)
|
||||
{
|
||||
QString text;
|
||||
if (active) {
|
||||
text = QString(obs_module_text("Stop"));
|
||||
} else {
|
||||
text = QString(obs_module_text("Start"));
|
||||
}
|
||||
|
||||
ui->previewOutputButton->setChecked(active);
|
||||
ui->previewOutputButton->setText(text);
|
||||
}
|
||||
|
||||
static obs_properties_t *create_misc_props_ui(void *vp)
|
||||
{
|
||||
AJAOutputUI *outputUI = (AJAOutputUI *)vp;
|
||||
if (!outputUI)
|
||||
return nullptr;
|
||||
aja::CardManager *cardManager = outputUI->GetCardManager();
|
||||
if (!cardManager)
|
||||
return nullptr;
|
||||
|
||||
bool haveMultiView = false;
|
||||
for (auto &c : *cardManager) {
|
||||
auto deviceID = c.second->GetDeviceID();
|
||||
for (const auto &id : aja::MultiViewCards()) {
|
||||
if (deviceID == id) {
|
||||
haveMultiView = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
obs_properties_t *props = obs_properties_create();
|
||||
obs_property_t *deviceList = obs_properties_add_list(props, kUIPropDevice.id,
|
||||
obs_module_text(kUIPropDevice.text), OBS_COMBO_TYPE_LIST,
|
||||
OBS_COMBO_FORMAT_STRING);
|
||||
obs_property_t *multiViewEnable =
|
||||
obs_properties_add_bool(props, kUIPropMultiViewEnable.id, obs_module_text(kUIPropMultiViewEnable.text));
|
||||
obs_property_t *multiViewAudioSources = obs_properties_add_list(
|
||||
props, kUIPropMultiViewAudioSource.id, obs_module_text(kUIPropMultiViewAudioSource.text),
|
||||
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
|
||||
|
||||
obs_property_list_clear(deviceList);
|
||||
obs_property_list_clear(multiViewAudioSources);
|
||||
|
||||
NTV2DeviceID firstDeviceID = DEVICE_ID_NOTFOUND;
|
||||
populate_misc_device_list(deviceList, cardManager, firstDeviceID);
|
||||
populate_multi_view_audio_sources(multiViewAudioSources, firstDeviceID);
|
||||
obs_property_set_modified_callback2(deviceList, on_misc_device_selected, cardManager);
|
||||
obs_property_set_modified_callback2(multiViewEnable, on_multi_view_toggle, cardManager);
|
||||
obs_property_set_modified_callback2(multiViewAudioSources, on_multi_view_toggle, cardManager);
|
||||
|
||||
outputUI->ui->label_3->setVisible(haveMultiView);
|
||||
obs_property_set_visible(deviceList, haveMultiView);
|
||||
obs_property_set_visible(multiViewEnable, haveMultiView);
|
||||
obs_property_set_visible(multiViewAudioSources, haveMultiView);
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
void AJAOutputUI::MiscPropertiesChanged()
|
||||
{
|
||||
SaveSettings(kMiscPropsFilename, miscPropertiesView->GetSettings());
|
||||
}
|
||||
|
||||
void AJAOutputUI::SetCardManager(aja::CardManager *cm)
|
||||
{
|
||||
cardManager = cm;
|
||||
}
|
||||
|
||||
aja::CardManager *AJAOutputUI::GetCardManager()
|
||||
{
|
||||
return cardManager;
|
||||
}
|
||||
|
||||
void AJAOutputUI::SetupMiscPropertiesView()
|
||||
{
|
||||
if (miscPropertiesView)
|
||||
delete miscPropertiesView;
|
||||
|
||||
obs_data_t *settings = obs_data_create();
|
||||
OBSData data = load_settings(kMiscPropsFilename);
|
||||
if (data) {
|
||||
obs_data_apply(settings, data);
|
||||
}
|
||||
|
||||
miscPropertiesView = new OBSPropertiesView(settings, this, (PropertiesReloadCallback)create_misc_props_ui,
|
||||
nullptr, nullptr, 170);
|
||||
|
||||
ui->miscPropertiesLayout->addWidget(miscPropertiesView);
|
||||
obs_data_release(settings);
|
||||
connect(miscPropertiesView, &OBSPropertiesView::Changed, this, &AJAOutputUI::MiscPropertiesChanged);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <properties-view.hpp>
|
||||
|
||||
#include "ui_output.h"
|
||||
|
||||
namespace aja {
|
||||
class CardManager;
|
||||
}
|
||||
|
||||
class AJAOutputUI : public QDialog {
|
||||
Q_OBJECT
|
||||
private:
|
||||
OBSPropertiesView *propertiesView;
|
||||
OBSPropertiesView *previewPropertiesView;
|
||||
OBSPropertiesView *miscPropertiesView;
|
||||
aja::CardManager *cardManager;
|
||||
public slots:
|
||||
void on_outputButton_clicked();
|
||||
void PropertiesChanged();
|
||||
void OutputStateChanged(bool);
|
||||
|
||||
void on_previewOutputButton_clicked();
|
||||
void PreviewPropertiesChanged();
|
||||
void PreviewOutputStateChanged(bool);
|
||||
|
||||
void MiscPropertiesChanged();
|
||||
|
||||
public:
|
||||
std::unique_ptr<Ui_Output> ui;
|
||||
AJAOutputUI(QWidget *parent);
|
||||
|
||||
void SetCardManager(aja::CardManager *cm);
|
||||
aja::CardManager *GetCardManager();
|
||||
|
||||
void ShowHideDialog();
|
||||
void SaveSettings(const char *filename, obs_data_t *settings);
|
||||
void SetupPropertiesView();
|
||||
void SetupPreviewPropertiesView();
|
||||
void SetupMiscPropertiesView();
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
cmake_minimum_required(VERSION 3.28...3.30)
|
||||
|
||||
if(NOT ENABLE_AJA)
|
||||
target_disable(aja-output-ui)
|
||||
return()
|
||||
endif()
|
||||
|
||||
find_package(LibAJANTV2 REQUIRED)
|
||||
|
||||
find_package(Qt6 REQUIRED Widgets)
|
||||
|
||||
if(OS_LINUX OR OS_FREEBSD OR OS_OPENBSD)
|
||||
find_package(Qt6 REQUIRED Gui)
|
||||
|
||||
find_package(X11 REQUIRED)
|
||||
endif()
|
||||
|
||||
if(NOT TARGET OBS::aja-support)
|
||||
add_subdirectory("${CMAKE_SOURCE_DIR}/plugins/aja" "${CMAKE_BINARY_DIR}/plugins/aja")
|
||||
endif()
|
||||
|
||||
if(NOT TARGET OBS::properties-view)
|
||||
add_subdirectory("${CMAKE_SOURCE_DIR}/shared/properties-view" "${CMAKE_BINARY_DIR}/shared/properties-view")
|
||||
endif()
|
||||
|
||||
add_library(aja-output-ui MODULE)
|
||||
add_library(OBS::aja-output-ui ALIAS aja-output-ui)
|
||||
|
||||
target_sources(aja-output-ui PRIVATE aja-ui-main.cpp aja-ui-main.h AJAOutputUI.cpp AJAOutputUI.h)
|
||||
|
||||
target_sources(aja-output-ui PRIVATE forms/output.ui)
|
||||
|
||||
target_compile_options(
|
||||
aja-output-ui
|
||||
PRIVATE
|
||||
$<$<COMPILE_LANG_AND_ID:CXX,AppleClang,Clang>:-Wno-deprecated-declarations>
|
||||
$<$<COMPILE_LANG_AND_ID:CXX,AppleClang,Clang>:-Wno-quoted-include-in-framework-header>
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
aja-output-ui
|
||||
PRIVATE
|
||||
OBS::libobs
|
||||
OBS::aja-support
|
||||
OBS::frontend-api
|
||||
OBS::properties-view
|
||||
Qt::Widgets
|
||||
AJA::LibAJANTV2
|
||||
$<$<PLATFORM_ID:Linux,FreeBSD,OpenBSD>:X11::X11>
|
||||
$<$<PLATFORM_ID:Linux,FreeBSD,OpenBSD>:Qt::GuiPrivate>
|
||||
)
|
||||
|
||||
target_link_options(aja-output-ui PRIVATE $<$<PLATFORM_ID:Windows>:/IGNORE:4099>)
|
||||
|
||||
if(OS_WINDOWS)
|
||||
configure_file(cmake/windows/obs-module.rc.in aja-output-ui.rc)
|
||||
target_sources(aja-output-ui PRIVATE aja-output-ui.rc)
|
||||
|
||||
set_property(TARGET aja-output-ui APPEND PROPERTY AUTORCC_OPTIONS --format-version 1)
|
||||
endif()
|
||||
|
||||
set_target_properties_obs(
|
||||
aja-output-ui
|
||||
PROPERTIES FOLDER frontend
|
||||
PREFIX ""
|
||||
AUTOMOC ON
|
||||
AUTOUIC ON
|
||||
AUTORCC ON
|
||||
AUTOUIC_SEARCH_PATHS forms
|
||||
)
|
||||
@@ -0,0 +1,430 @@
|
||||
#include "aja-ui-main.h"
|
||||
#include "AJAOutputUI.h"
|
||||
|
||||
#include "../../../plugins/aja/aja-ui-props.hpp"
|
||||
#include "../../../plugins/aja/aja-card-manager.hpp"
|
||||
#include "../../../plugins/aja/aja-routing.hpp"
|
||||
|
||||
#include <obs-module.h>
|
||||
#include <obs-frontend-api.h>
|
||||
#include <QMainWindow>
|
||||
#include <QAction>
|
||||
#include <util/util.hpp>
|
||||
#include <util/platform.h>
|
||||
#include <media-io/video-io.h>
|
||||
#include <media-io/video-frame.h>
|
||||
#include <ajantv2/includes/ntv2devicescanner.h>
|
||||
|
||||
OBS_DECLARE_MODULE()
|
||||
OBS_MODULE_USE_DEFAULT_LOCALE("aja-output-ui", "en-US")
|
||||
|
||||
static aja::CardManager *cardManager = nullptr;
|
||||
static AJAOutputUI *ajaOutputUI = nullptr;
|
||||
static obs_output_t *output = nullptr;
|
||||
|
||||
bool main_output_running = false;
|
||||
bool preview_output_running = false;
|
||||
|
||||
struct preview_output {
|
||||
bool enabled;
|
||||
obs_source_t *current_source;
|
||||
obs_output_t *output;
|
||||
|
||||
video_t *video_queue;
|
||||
gs_texrender_t *texrender;
|
||||
gs_stagesurf_t *stagesurface;
|
||||
uint8_t *video_data;
|
||||
uint32_t video_linesize;
|
||||
|
||||
obs_video_info ovi;
|
||||
};
|
||||
|
||||
static struct preview_output context = {0};
|
||||
|
||||
OBSData load_settings(const char *filename)
|
||||
{
|
||||
BPtr<char> path = obs_module_get_config_path(obs_current_module(), filename);
|
||||
BPtr<char> jsonData = os_quick_read_utf8_file(path);
|
||||
if (!!jsonData) {
|
||||
obs_data_t *data = obs_data_create_from_json(jsonData);
|
||||
OBSData dataRet(data);
|
||||
obs_data_release(data);
|
||||
|
||||
return dataRet;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void output_stop()
|
||||
{
|
||||
obs_output_stop(output);
|
||||
obs_output_release(output);
|
||||
main_output_running = false;
|
||||
ajaOutputUI->OutputStateChanged(false);
|
||||
}
|
||||
|
||||
void output_start()
|
||||
{
|
||||
OBSData settings = load_settings(kProgramPropsFilename);
|
||||
|
||||
if (settings != nullptr) {
|
||||
output = obs_output_create("aja_output", kProgramOutputID, settings, NULL);
|
||||
|
||||
bool started = obs_output_start(output);
|
||||
obs_data_release(settings);
|
||||
|
||||
main_output_running = started;
|
||||
|
||||
ajaOutputUI->OutputStateChanged(started);
|
||||
|
||||
if (!started)
|
||||
output_stop();
|
||||
}
|
||||
}
|
||||
|
||||
void output_toggle()
|
||||
{
|
||||
if (main_output_running)
|
||||
output_stop();
|
||||
else
|
||||
output_start();
|
||||
}
|
||||
|
||||
void on_preview_scene_changed(enum obs_frontend_event event, void *param);
|
||||
void render_preview_source(void *param, uint32_t cx, uint32_t cy);
|
||||
|
||||
void preview_output_stop()
|
||||
{
|
||||
obs_output_stop(context.output);
|
||||
obs_output_release(context.output);
|
||||
|
||||
obs_remove_main_render_callback(render_preview_source, &context);
|
||||
obs_frontend_remove_event_callback(on_preview_scene_changed, &context);
|
||||
|
||||
obs_source_release(context.current_source);
|
||||
|
||||
obs_enter_graphics();
|
||||
gs_stagesurface_destroy(context.stagesurface);
|
||||
gs_texrender_destroy(context.texrender);
|
||||
obs_leave_graphics();
|
||||
|
||||
video_output_close(context.video_queue);
|
||||
|
||||
preview_output_running = false;
|
||||
ajaOutputUI->PreviewOutputStateChanged(false);
|
||||
}
|
||||
|
||||
void preview_output_start()
|
||||
{
|
||||
OBSData settings = load_settings(kPreviewPropsFilename);
|
||||
|
||||
if (settings != nullptr) {
|
||||
context.output = obs_output_create("aja_output", kPreviewOutputID, settings, NULL);
|
||||
|
||||
obs_get_video_info(&context.ovi);
|
||||
|
||||
uint32_t width = context.ovi.base_width;
|
||||
uint32_t height = context.ovi.base_height;
|
||||
|
||||
obs_enter_graphics();
|
||||
context.texrender = gs_texrender_create(GS_BGRA, GS_ZS_NONE);
|
||||
context.stagesurface = gs_stagesurface_create(width, height, GS_BGRA);
|
||||
obs_leave_graphics();
|
||||
|
||||
const video_output_info *mainVOI = video_output_get_info(obs_get_video());
|
||||
|
||||
video_output_info vi = {0};
|
||||
vi.format = VIDEO_FORMAT_BGRA;
|
||||
vi.width = width;
|
||||
vi.height = height;
|
||||
vi.fps_den = context.ovi.fps_den;
|
||||
vi.fps_num = context.ovi.fps_num;
|
||||
vi.cache_size = 16;
|
||||
vi.colorspace = mainVOI->colorspace;
|
||||
vi.range = mainVOI->range;
|
||||
vi.name = kPreviewOutputID;
|
||||
|
||||
video_output_open(&context.video_queue, &vi);
|
||||
|
||||
obs_frontend_add_event_callback(on_preview_scene_changed, &context);
|
||||
if (obs_frontend_preview_program_mode_active()) {
|
||||
context.current_source = obs_frontend_get_current_preview_scene();
|
||||
} else {
|
||||
context.current_source = obs_frontend_get_current_scene();
|
||||
}
|
||||
obs_add_main_render_callback(render_preview_source, &context);
|
||||
|
||||
obs_output_set_media(context.output, context.video_queue, obs_get_audio());
|
||||
bool started = obs_output_start(context.output);
|
||||
|
||||
obs_data_release(settings);
|
||||
|
||||
preview_output_running = started;
|
||||
ajaOutputUI->PreviewOutputStateChanged(started);
|
||||
|
||||
if (!started)
|
||||
preview_output_stop();
|
||||
}
|
||||
}
|
||||
|
||||
void preview_output_toggle()
|
||||
{
|
||||
if (preview_output_running)
|
||||
preview_output_stop();
|
||||
else
|
||||
preview_output_start();
|
||||
}
|
||||
|
||||
void populate_misc_device_list(obs_property_t *list, aja::CardManager *cardManager, NTV2DeviceID &firstDeviceID)
|
||||
{
|
||||
for (const auto &iter : *cardManager) {
|
||||
if (!iter.second)
|
||||
continue;
|
||||
if (firstDeviceID == DEVICE_ID_NOTFOUND)
|
||||
firstDeviceID = iter.second->GetDeviceID();
|
||||
obs_property_list_add_string(list, iter.second->GetDisplayName().c_str(),
|
||||
iter.second->GetCardID().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void populate_multi_view_audio_sources(obs_property_t *list, NTV2DeviceID id)
|
||||
{
|
||||
obs_property_list_clear(list);
|
||||
const QList<NTV2InputSource> kMultiViewAudioInputs = {
|
||||
NTV2_INPUTSOURCE_SDI1,
|
||||
NTV2_INPUTSOURCE_SDI2,
|
||||
NTV2_INPUTSOURCE_SDI3,
|
||||
NTV2_INPUTSOURCE_SDI4,
|
||||
};
|
||||
for (const auto &inp : kMultiViewAudioInputs) {
|
||||
if (NTV2DeviceCanDoInputSource(id, inp)) {
|
||||
std::string inputSourceStr = NTV2InputSourceToString(inp, true);
|
||||
obs_property_list_add_int(list, inputSourceStr.c_str(), (long long)inp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool on_misc_device_selected(void *data, obs_properties_t *props, obs_property_t *, obs_data_t *settings)
|
||||
{
|
||||
const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
|
||||
if (!cardID || !cardID[0])
|
||||
return false;
|
||||
aja::CardManager *cardManager = (aja::CardManager *)data;
|
||||
if (!cardManager)
|
||||
return false;
|
||||
auto cardEntry = cardManager->GetCardEntry(cardID);
|
||||
if (!cardEntry)
|
||||
return false;
|
||||
|
||||
NTV2DeviceID deviceID = cardEntry->GetDeviceID();
|
||||
bool enableMultiViewUI = NTV2DeviceCanDoHDMIMultiView(deviceID);
|
||||
obs_property_t *multiViewCheckbox = obs_properties_get(props, kUIPropMultiViewEnable.id);
|
||||
obs_property_t *multiViewAudioSource = obs_properties_get(props, kUIPropMultiViewAudioSource.id);
|
||||
populate_multi_view_audio_sources(multiViewAudioSource, deviceID);
|
||||
obs_property_set_enabled(multiViewCheckbox, enableMultiViewUI);
|
||||
obs_property_set_enabled(multiViewAudioSource, enableMultiViewUI);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void toggle_multi_view(CNTV2Card *card, NTV2InputSource src, bool enable)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
std::string datastream = std::to_string(i);
|
||||
oss << "sdi[" << datastream << "][0]->hdmi[0][" << datastream << "];";
|
||||
}
|
||||
|
||||
NTV2DeviceID deviceId = card->GetDeviceID();
|
||||
NTV2AudioSystem audioSys = NTV2InputSourceToAudioSystem(src);
|
||||
if (NTV2DeviceCanDoHDMIMultiView(deviceId)) {
|
||||
NTV2XptConnections cnx;
|
||||
if (aja::Routing::ParseRouteString(oss.str(), cnx)) {
|
||||
card->SetMultiRasterBypassEnable(!enable);
|
||||
if (enable) {
|
||||
card->ApplySignalRoute(cnx, false);
|
||||
if (NTV2DeviceCanDoAudioMixer(deviceId)) {
|
||||
card->SetAudioMixerInputAudioSystem(NTV2_AudioMixerInputMain, audioSys);
|
||||
card->SetAudioMixerInputChannelSelect(NTV2_AudioMixerInputMain,
|
||||
NTV2_AudioChannel1_2);
|
||||
card->SetAudioMixerInputChannelsMute(NTV2_AudioMixerInputAux1,
|
||||
NTV2AudioChannelsMuteAll);
|
||||
card->SetAudioMixerInputChannelsMute(NTV2_AudioMixerInputAux2,
|
||||
NTV2AudioChannelsMuteAll);
|
||||
}
|
||||
card->SetAudioLoopBack(NTV2_AUDIO_LOOPBACK_ON, audioSys);
|
||||
card->SetAudioOutputMonitorSource(NTV2_AudioChannel1_2, audioSys);
|
||||
card->SetHDMIOutAudioChannels(NTV2_HDMIAudio8Channels);
|
||||
card->SetHDMIOutAudioSource2Channel(NTV2_AudioChannel1_2, audioSys);
|
||||
card->SetHDMIOutAudioSource8Channel(NTV2_AudioChannel1_8, audioSys);
|
||||
} else {
|
||||
card->RemoveConnections(cnx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool on_multi_view_toggle(void *data, obs_properties_t *, obs_property_t *, obs_data_t *settings)
|
||||
{
|
||||
bool multiViewEnabled = obs_data_get_bool(settings, kUIPropMultiViewEnable.id) && !main_output_running &&
|
||||
!preview_output_running;
|
||||
const int audioInputSource = obs_data_get_int(settings, kUIPropMultiViewAudioSource.id);
|
||||
const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
|
||||
if (!cardID || !cardID[0])
|
||||
return false;
|
||||
aja::CardManager *cardManager = (aja::CardManager *)data;
|
||||
if (!cardManager)
|
||||
return false;
|
||||
CNTV2Card *card = cardManager->GetCard(cardID);
|
||||
if (!card)
|
||||
return false;
|
||||
|
||||
NTV2InputSource inputSource = (NTV2InputSource)audioInputSource;
|
||||
toggle_multi_view(card, inputSource, multiViewEnabled);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// MISC PROPS
|
||||
|
||||
void on_preview_scene_changed(enum obs_frontend_event event, void *param)
|
||||
{
|
||||
auto ctx = (struct preview_output *)param;
|
||||
switch (event) {
|
||||
case OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED:
|
||||
case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED:
|
||||
obs_source_release(ctx->current_source);
|
||||
ctx->current_source = obs_frontend_get_current_preview_scene();
|
||||
break;
|
||||
case OBS_FRONTEND_EVENT_STUDIO_MODE_DISABLED:
|
||||
obs_source_release(ctx->current_source);
|
||||
ctx->current_source = obs_frontend_get_current_scene();
|
||||
break;
|
||||
case OBS_FRONTEND_EVENT_SCENE_CHANGED:
|
||||
if (!obs_frontend_preview_program_mode_active()) {
|
||||
obs_source_release(ctx->current_source);
|
||||
ctx->current_source = obs_frontend_get_current_scene();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void render_preview_source(void *param, uint32_t, uint32_t)
|
||||
{
|
||||
auto ctx = (struct preview_output *)param;
|
||||
|
||||
if (!ctx->current_source)
|
||||
return;
|
||||
|
||||
uint32_t width = obs_source_get_base_width(ctx->current_source);
|
||||
uint32_t height = obs_source_get_base_height(ctx->current_source);
|
||||
|
||||
gs_texrender_reset(ctx->texrender);
|
||||
|
||||
if (gs_texrender_begin(ctx->texrender, width, height)) {
|
||||
struct vec4 background;
|
||||
vec4_zero(&background);
|
||||
|
||||
gs_clear(GS_CLEAR_COLOR, &background, 0.0f, 0);
|
||||
gs_ortho(0.0f, (float)width, 0.0f, (float)height, -100.0f, 100.0f);
|
||||
|
||||
gs_blend_state_push();
|
||||
gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO);
|
||||
|
||||
obs_source_video_render(ctx->current_source);
|
||||
|
||||
gs_blend_state_pop();
|
||||
gs_texrender_end(ctx->texrender);
|
||||
|
||||
struct video_frame output_frame;
|
||||
if (video_output_lock_frame(ctx->video_queue, &output_frame, 1, os_gettime_ns())) {
|
||||
gs_stage_texture(ctx->stagesurface, gs_texrender_get_texture(ctx->texrender));
|
||||
|
||||
if (gs_stagesurface_map(ctx->stagesurface, &ctx->video_data, &ctx->video_linesize)) {
|
||||
uint32_t linesize = output_frame.linesize[0];
|
||||
for (uint32_t i = 0; i < ctx->ovi.base_height; i++) {
|
||||
uint32_t dst_offset = linesize * i;
|
||||
uint32_t src_offset = ctx->video_linesize * i;
|
||||
memcpy(output_frame.data[0] + dst_offset, ctx->video_data + src_offset,
|
||||
linesize);
|
||||
}
|
||||
|
||||
gs_stagesurface_unmap(ctx->stagesurface);
|
||||
ctx->video_data = nullptr;
|
||||
}
|
||||
|
||||
video_output_unlock_frame(ctx->video_queue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addOutputUI(void)
|
||||
{
|
||||
QAction *action = (QAction *)obs_frontend_add_tools_menu_qaction("AJA I/O Device Output");
|
||||
|
||||
QMainWindow *window = (QMainWindow *)obs_frontend_get_main_window();
|
||||
|
||||
obs_frontend_push_ui_translation(obs_module_get_string);
|
||||
ajaOutputUI = new AJAOutputUI(window);
|
||||
obs_frontend_pop_ui_translation();
|
||||
|
||||
auto cb = []() {
|
||||
ajaOutputUI->ShowHideDialog();
|
||||
};
|
||||
|
||||
action->connect(action, &QAction::triggered, cb);
|
||||
}
|
||||
|
||||
static void OBSEvent(enum obs_frontend_event event, void *)
|
||||
{
|
||||
if (event == OBS_FRONTEND_EVENT_FINISHED_LOADING) {
|
||||
OBSData settings = load_settings(kProgramPropsFilename);
|
||||
if (settings && obs_data_get_bool(settings, kUIPropAutoStartOutput.id))
|
||||
output_start();
|
||||
|
||||
OBSData previewSettings = load_settings(kPreviewPropsFilename);
|
||||
if (previewSettings && obs_data_get_bool(previewSettings, kUIPropAutoStartOutput.id))
|
||||
preview_output_start();
|
||||
|
||||
OBSData miscSettings = load_settings(kMiscPropsFilename);
|
||||
if (miscSettings && ajaOutputUI) {
|
||||
on_multi_view_toggle(ajaOutputUI->GetCardManager(), nullptr, nullptr, miscSettings);
|
||||
}
|
||||
} else if (event == OBS_FRONTEND_EVENT_EXIT) {
|
||||
if (main_output_running)
|
||||
output_stop();
|
||||
if (preview_output_running)
|
||||
preview_output_stop();
|
||||
}
|
||||
}
|
||||
|
||||
static void aja_loaded(void * /* data */, calldata_t *calldata)
|
||||
{
|
||||
// Receive CardManager pointer from the main AJA plugin
|
||||
calldata_get_ptr(calldata, "card_manager", &cardManager);
|
||||
if (ajaOutputUI)
|
||||
ajaOutputUI->SetCardManager(cardManager);
|
||||
}
|
||||
|
||||
bool obs_module_load(void)
|
||||
{
|
||||
CNTV2DeviceScanner scanner;
|
||||
auto numDevices = scanner.GetNumDevices();
|
||||
if (numDevices == 0) {
|
||||
blog(LOG_WARNING, "No AJA devices found, skipping loading AJA UI plugin");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Signal to wait for AJA plugin to finish loading so we can access the CardManager instance
|
||||
auto signal_handler = obs_get_signal_handler();
|
||||
signal_handler_add(signal_handler, "void aja_loaded(ptr card_manager)");
|
||||
signal_handler_connect(signal_handler, "aja_loaded", aja_loaded, nullptr);
|
||||
|
||||
addOutputUI();
|
||||
|
||||
obs_frontend_add_event_callback(OBSEvent, nullptr);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <obs.hpp>
|
||||
|
||||
#include <ajantv2/includes/ntv2enums.h>
|
||||
namespace aja {
|
||||
class CardManager;
|
||||
}
|
||||
|
||||
static const char *kProgramPropsFilename = "ajaOutputProps.json";
|
||||
static const char *kPreviewPropsFilename = "ajaPreviewOutputProps.json";
|
||||
static const char *kMiscPropsFilename = "ajaMiscProps.json";
|
||||
|
||||
OBSData load_settings(const char *filename);
|
||||
void output_toggle();
|
||||
void preview_output_toggle();
|
||||
void populate_misc_device_list(obs_property_t *list, aja::CardManager *cardManager, NTV2DeviceID &firstDeviceID);
|
||||
void populate_multi_view_audio_sources(obs_property_t *list, NTV2DeviceID id);
|
||||
bool on_misc_device_selected(void *data, obs_properties_t *props, obs_property_t *list, obs_data_t *settings);
|
||||
bool on_multi_view_toggle(void *data, obs_properties_t *props, obs_property_t *list, obs_data_t *settings);
|
||||
@@ -0,0 +1,24 @@
|
||||
1 VERSIONINFO
|
||||
FILEVERSION ${OBS_VERSION_MAJOR},${OBS_VERSION_MINOR},${OBS_VERSION_PATCH},0
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904B0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "${OBS_COMPANY_NAME}"
|
||||
VALUE "FileDescription", "OBS AJA Output UI"
|
||||
VALUE "FileVersion", "${OBS_VERSION_CANONICAL}"
|
||||
VALUE "ProductName", "${OBS_PRODUCT_NAME}"
|
||||
VALUE "ProductVersion", "${OBS_VERSION_CANONICAL}"
|
||||
VALUE "Comments", "${OBS_COMMENTS}"
|
||||
VALUE "LegalCopyright", "${OBS_LEGAL_COPYRIGHT}"
|
||||
VALUE "InternalName", "aja-output-ui"
|
||||
VALUE "OriginalFilename", "aja-output-ui"
|
||||
END
|
||||
END
|
||||
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x0409, 0x04B0
|
||||
END
|
||||
END
|
||||
@@ -0,0 +1,4 @@
|
||||
AJAOutput.Device="AJA I/O-toestelafvoer"
|
||||
AJAOutput.ProgramOutput="Programafvoer"
|
||||
AJAOutput.PreviewOutput="Voorskou-afvoer"
|
||||
AJAOutput.MiscOutput="Bykomende instellings"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="مَخرج جهاز AJA I/O"
|
||||
AJAOutput.ProgramOutput="مَخرج البرنامج"
|
||||
AJAOutput.PreviewOutput="مَخرج المعاينة"
|
||||
AJAOutput.MiscOutput="إعدادات إضافية"
|
||||
AJAOutput.MultiViewEnable="تمكين عرض متعدد"
|
||||
AJAOutput.MultiViewAudioSource="مصدر الصوت للعرض المتعدد"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O Cihaz Çıxışı"
|
||||
AJAOutput.ProgramOutput="Proqram çıxışı"
|
||||
AJAOutput.PreviewOutput="Önizləmə çıxışı"
|
||||
AJAOutput.MiscOutput="Əlavə Parametrlər"
|
||||
AJAOutput.MultiViewEnable="Çoxlu Görünüşü Aktivləşdir"
|
||||
AJAOutput.MultiViewAudioSource="Çoxbaxışlı Səs Mənbəsi"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Захоп прылады AJA I/O"
|
||||
AJAOutput.ProgramOutput="Праграмны вывад"
|
||||
AJAOutput.PreviewOutput="Прадпрагляд вываду"
|
||||
AJAOutput.MiscOutput="Дадатковыя налады"
|
||||
AJAOutput.MultiViewEnable="Уключыць мультыпрагляд"
|
||||
AJAOutput.MultiViewAudioSource="Мультыпрагляд крыніц аўдыя"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Изход на устройството AJA I/O"
|
||||
AJAOutput.ProgramOutput="Програмен изход"
|
||||
AJAOutput.PreviewOutput="Нагледно изхода"
|
||||
AJAOutput.MiscOutput="Допълнителни настройки"
|
||||
AJAOutput.MultiViewEnable="Включване на множко изгледи"
|
||||
AJAOutput.MultiViewAudioSource="Източник на звука за множко изгледи"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Dispositiu de sortida d'E/S AJA"
|
||||
AJAOutput.ProgramOutput="Sortida del programa"
|
||||
AJAOutput.PreviewOutput="Sortida de la vista prèvia"
|
||||
AJAOutput.MiscOutput="Configuració addicional"
|
||||
AJAOutput.MultiViewEnable="Habilita la visualització múltiple"
|
||||
AJAOutput.MultiViewAudioSource="Font d'àudio de visualització múltiple"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Výstup AJA I/O zařízení"
|
||||
AJAOutput.ProgramOutput="Výstup programu"
|
||||
AJAOutput.PreviewOutput="Náhled výstupu"
|
||||
AJAOutput.MiscOutput="Další nastavení"
|
||||
AJAOutput.MultiViewEnable="Povolit Multiview"
|
||||
AJAOutput.MultiViewAudioSource="Zdroj zvuku pro Multiview"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O-enhedsoutput"
|
||||
AJAOutput.ProgramOutput="Programoutput"
|
||||
AJAOutput.PreviewOutput="Forhåndsvisningsoutput"
|
||||
AJAOutput.MiscOutput="Yderligere indstillinger"
|
||||
AJAOutput.MultiViewEnable="Aktivér Multivisning"
|
||||
AJAOutput.MultiViewAudioSource="Multivisning Lydkilde"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA-I/O-Geräteausgabe"
|
||||
AJAOutput.ProgramOutput="Programmausgabe"
|
||||
AJAOutput.PreviewOutput="Vorschauausgabe"
|
||||
AJAOutput.MiscOutput="Weitere Einstellungen"
|
||||
AJAOutput.MultiViewEnable="Multiview aktivieren"
|
||||
AJAOutput.MultiViewAudioSource="Multiview-Audioquelle"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Συσκευή εξόδου I/O AJA"
|
||||
AJAOutput.ProgramOutput="Έξοδος Προγράμματος"
|
||||
AJAOutput.PreviewOutput="Προεπισκόπηση Εξόδου"
|
||||
AJAOutput.MiscOutput="Πρόσθετες Ρυθμίσεις"
|
||||
AJAOutput.MultiViewEnable="Ενεργοποίηση Πολλαπλής Προβολής"
|
||||
AJAOutput.MultiViewAudioSource="Πηγή Ήχου Πολλαπλής Προβολής"
|
||||
@@ -0,0 +1 @@
|
||||
#
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O Device Output"
|
||||
AJAOutput.ProgramOutput="Program Output"
|
||||
AJAOutput.PreviewOutput="Preview Output"
|
||||
AJAOutput.MiscOutput="Additional Settings"
|
||||
AJAOutput.MultiViewEnable="Enable Multi View"
|
||||
AJAOutput.MultiViewAudioSource="Multi View Audio Source"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Salida de dispositivo de E/S AJA"
|
||||
AJAOutput.ProgramOutput="Salida del programa"
|
||||
AJAOutput.PreviewOutput="Salida de la vista previa"
|
||||
AJAOutput.MiscOutput="Ajustes adicionales"
|
||||
AJAOutput.MultiViewEnable="Habilitar vista múltiple"
|
||||
AJAOutput.MultiViewAudioSource="Fuente de audio de la vista múltiple"
|
||||
@@ -0,0 +1,3 @@
|
||||
AJAOutput.ProgramOutput="Programmi väljund"
|
||||
AJAOutput.PreviewOutput="Eelvaate väljund"
|
||||
AJAOutput.MiscOutput="Lisaseaded"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O Irteera pantaila-argazki gailua"
|
||||
AJAOutput.ProgramOutput="Irteera programa"
|
||||
AJAOutput.PreviewOutput="Irteera aurrerakina"
|
||||
AJAOutput.MiscOutput="Ezarpen gehigarriak"
|
||||
AJAOutput.MultiViewEnable="Ikuspuntu anizkoitza baimendu"
|
||||
AJAOutput.MultiViewAudioSource="Entzunezko iturburuaren ikuspuntu anizkoitza"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="خروجی دستگاه و/خ AJA"
|
||||
AJAOutput.ProgramOutput="خروجی برنامه"
|
||||
AJAOutput.PreviewOutput="پیش نمایش خروجی"
|
||||
AJAOutput.MiscOutput="تنظیمات اضافی"
|
||||
AJAOutput.MultiViewEnable="چند نمای را فعال کنید"
|
||||
AJAOutput.MultiViewAudioSource="منبع صوتی چند نما"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O -laitteen ulostulo"
|
||||
AJAOutput.ProgramOutput="Ohjelman ulostulo"
|
||||
AJAOutput.PreviewOutput="Esikatselun ulostulo"
|
||||
AJAOutput.MiscOutput="Lisäasetukset"
|
||||
AJAOutput.MultiViewEnable="Ota moninäkymä käyttöön"
|
||||
AJAOutput.MultiViewAudioSource="Moninäkymän äänilähde"
|
||||
@@ -0,0 +1,3 @@
|
||||
AJAOutput.PreviewOutput="Silipin Ang Output"
|
||||
AJAOutput.MiscOutput="Karagdagang kaayusan"
|
||||
AJAOutput.MultiViewEnable="Paganahin ang Multi View"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Périphérique de sortie I/O AJA"
|
||||
AJAOutput.ProgramOutput="Sortie du programme"
|
||||
AJAOutput.PreviewOutput="Sortie de l'aperçu"
|
||||
AJAOutput.MiscOutput="Paramètres supplémentaires"
|
||||
AJAOutput.MultiViewEnable="Activer Multi View"
|
||||
AJAOutput.MultiViewAudioSource="Source audio de la sortie Multi View"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Dispositivo de Saida AJA I/O"
|
||||
AJAOutput.ProgramOutput="Programa saida"
|
||||
AJAOutput.PreviewOutput="Mostrar a Saida"
|
||||
AJAOutput.MiscOutput="Configuracions adiccionais"
|
||||
AJAOutput.MultiViewEnable="Activar Multiples Vistas"
|
||||
AJAOutput.MultiViewAudioSource="Fonte de son Multi vistas"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="פלט התקן AJA I/O"
|
||||
AJAOutput.ProgramOutput="פלט תכנית"
|
||||
AJAOutput.PreviewOutput="פלט תצוגה מקדימה"
|
||||
AJAOutput.MiscOutput="הגדרות נוספות"
|
||||
AJAOutput.MultiViewEnable="הפעלת ריבוי תצוגות"
|
||||
AJAOutput.MultiViewAudioSource="מקור שמע של ריבוי תצוגות"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O डिवाइस आउटपुट"
|
||||
AJAOutput.ProgramOutput="प्रोग्राम आउटपुट"
|
||||
AJAOutput.PreviewOutput="पूर्वावलोकन आउटपुट"
|
||||
AJAOutput.MiscOutput="अतिरिक्त सेटिंग"
|
||||
AJAOutput.MultiViewEnable="अनेक दृश्य सक्षम करें"
|
||||
AJAOutput.MultiViewAudioSource="अनेक दृश्य ऑडियो स्रोत"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Pretpregled AJA I/O uređaja "
|
||||
AJAOutput.ProgramOutput="Pretpregled programa"
|
||||
AJAOutput.PreviewOutput="Pretpregled izlaznog rezultata"
|
||||
AJAOutput.MiscOutput="Dodatne postavke"
|
||||
AJAOutput.MultiViewEnable="Omogući višestruki prikaz"
|
||||
AJAOutput.MultiViewAudioSource="Višestruki prikaz zvučnog izvora "
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O eszközkimenet"
|
||||
AJAOutput.ProgramOutput="Programkimenet"
|
||||
AJAOutput.PreviewOutput="Előnézeti kimenet"
|
||||
AJAOutput.MiscOutput="További beállítások"
|
||||
AJAOutput.MultiViewEnable="Több nézet engedélyezése"
|
||||
AJAOutput.MultiViewAudioSource="Több nézet hangforrása"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O սարքի ելք"
|
||||
AJAOutput.ProgramOutput="Ծրագրային Ելքագրում"
|
||||
AJAOutput.PreviewOutput="Նախադիտման արդյունքը"
|
||||
AJAOutput.MiscOutput="Լրացուցիչ կարգավորումներ"
|
||||
AJAOutput.MultiViewEnable="Միացնել Բազմակի դիտումը"
|
||||
AJAOutput.MultiViewAudioSource="Բազմակի դիտումի աուդիոի աղբյուր"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Perangkat Keluaran AJA I/O"
|
||||
AJAOutput.ProgramOutput="Keluaran Program"
|
||||
AJAOutput.PreviewOutput="Keluaran Pratinjau"
|
||||
AJAOutput.MiscOutput="Pengaturan Tambahan"
|
||||
AJAOutput.MultiViewEnable="Aktifkan Tampilan Ganda"
|
||||
AJAOutput.MultiViewAudioSource="Tampilan Sumber Suara Ganda"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Dispositivo AJA I/O di uscita"
|
||||
AJAOutput.ProgramOutput="Programma di uscita"
|
||||
AJAOutput.PreviewOutput="Anteprima di uscita"
|
||||
AJAOutput.MiscOutput="Ulteriori impostazioni"
|
||||
AJAOutput.MultiViewEnable="Abilita Visualizzazione Multipla"
|
||||
AJAOutput.MultiViewAudioSource="Visualizzazione multipla della sorgente audio"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O デバイス出力"
|
||||
AJAOutput.ProgramOutput="プログラム出力"
|
||||
AJAOutput.PreviewOutput="プレビュー出力"
|
||||
AJAOutput.MiscOutput="詳細設定"
|
||||
AJAOutput.MultiViewEnable="マルチビューを有効にする"
|
||||
AJAOutput.MultiViewAudioSource="マルチビュー 音声ソース"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA-I/O-მოწყობილობით გამოტანა"
|
||||
AJAOutput.ProgramOutput="პროგრამის გამოტანა"
|
||||
AJAOutput.PreviewOutput="შეთვალიერების გამოტანა"
|
||||
AJAOutput.MiscOutput="დამატებითი პარამეტრები"
|
||||
AJAOutput.MultiViewEnable="მრავალხედიანის ჩართვა"
|
||||
AJAOutput.MultiViewAudioSource="მრავალხედიანი ხმოვანი წყარო"
|
||||
@@ -0,0 +1 @@
|
||||
AJAOutput.MiscOutput="Qosımsha sazlawlar"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Derana amûrê AJA I/O"
|
||||
AJAOutput.ProgramOutput="Derana bernameyê"
|
||||
AJAOutput.PreviewOutput="Derana pêşdîtinê"
|
||||
AJAOutput.MiscOutput="Sazkariyên vebijêrkî"
|
||||
AJAOutput.MultiViewEnable="Pir dîtin çalak bike"
|
||||
AJAOutput.MultiViewAudioSource="Çavkaniya dengê ya pir dîtin"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O 디바이스 출력"
|
||||
AJAOutput.ProgramOutput="프로그램 출력"
|
||||
AJAOutput.PreviewOutput="미리보기 출력"
|
||||
AJAOutput.MiscOutput="추가 설정"
|
||||
AJAOutput.MultiViewEnable="다중 화면 활성화"
|
||||
AJAOutput.MultiViewAudioSource="다중 화면 오디오 소스"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Output Peranti I/O AJA"
|
||||
AJAOutput.ProgramOutput="Output Program"
|
||||
AJAOutput.PreviewOutput="Output Pratonton"
|
||||
AJAOutput.MiscOutput="Tetapan Tambahan"
|
||||
AJAOutput.MultiViewEnable="Benarkan Pandangan Berbilang"
|
||||
AJAOutput.MultiViewAudioSource="Sumber Audio Pandangan Berbilang"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O Enhet Utgang"
|
||||
AJAOutput.ProgramOutput="Program Utgang"
|
||||
AJAOutput.PreviewOutput="Forhåndsvis Utgang"
|
||||
AJAOutput.MiscOutput="Tilleggsinnstillinger"
|
||||
AJAOutput.MultiViewEnable="Aktiver flervisning"
|
||||
AJAOutput.MultiViewAudioSource="Flere viser lydkilde"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O Apparaat Uitvoer"
|
||||
AJAOutput.ProgramOutput="Programma Uitvoer"
|
||||
AJAOutput.PreviewOutput="Preview uitvoer"
|
||||
AJAOutput.MiscOutput="Aanvullende instellingen"
|
||||
AJAOutput.MultiViewEnable="Multi weergave inschakelen"
|
||||
AJAOutput.MultiViewAudioSource="Multi weergave audiobron"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Wyjście AJA I/O"
|
||||
AJAOutput.ProgramOutput="Wyjście w aplikacji"
|
||||
AJAOutput.PreviewOutput="Podgląd wyjścia"
|
||||
AJAOutput.MiscOutput="Dodatkowe ustawienia"
|
||||
AJAOutput.MultiViewEnable="Włącz Multiview"
|
||||
AJAOutput.MultiViewAudioSource="Źródło dźwięku Multiview"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Saída do dispositivo I/O AJA"
|
||||
AJAOutput.ProgramOutput="Saída do programa"
|
||||
AJAOutput.PreviewOutput="Saída da pré-visualização"
|
||||
AJAOutput.MiscOutput="Configurações adicionais"
|
||||
AJAOutput.MultiViewEnable="Habilitar visualização múltipla"
|
||||
AJAOutput.MultiViewAudioSource="Fonte de áudio da visualização múltipla"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Dispositivo de saída AJA I/O"
|
||||
AJAOutput.ProgramOutput="Saída do Programa"
|
||||
AJAOutput.PreviewOutput="Saída de antevisão"
|
||||
AJAOutput.MiscOutput="Definições adicionais"
|
||||
AJAOutput.MultiViewEnable="Ativar multivisualização"
|
||||
AJAOutput.MultiViewAudioSource="Fonte de áudio de multivisualização"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Outputul dispozitivului AJA I/O"
|
||||
AJAOutput.ProgramOutput="Outputul programului"
|
||||
AJAOutput.PreviewOutput="Previzualizarea outputului"
|
||||
AJAOutput.MiscOutput="Setări adiționale"
|
||||
AJAOutput.MultiViewEnable="Activează vizualizarea multiplă"
|
||||
AJAOutput.MultiViewAudioSource="Sursă audio pentru vizualizare multiplă"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Захват устройства AJA I/O"
|
||||
AJAOutput.ProgramOutput="Вывод программы"
|
||||
AJAOutput.PreviewOutput="Предпросмотр вывода"
|
||||
AJAOutput.MiscOutput="Дополнительные настройки"
|
||||
AJAOutput.MultiViewEnable="Включить мультипросмотр"
|
||||
AJAOutput.MultiViewAudioSource="Мультипросмотр источника аудио"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="ඒජේඒ ආ/ප්ර උපාංගයේ ප්රතිදානය"
|
||||
AJAOutput.ProgramOutput="වැඩසටහනේ ප්රතිදානය"
|
||||
AJAOutput.PreviewOutput="පෙරදසුනේ ප්රතිදානය"
|
||||
AJAOutput.MiscOutput="අතිරේක සැකසුම්"
|
||||
AJAOutput.MultiViewEnable="බහුදැක්ම සබල කරන්න"
|
||||
AJAOutput.MultiViewAudioSource="බහුදැක්ම ශ්රව්ය මූලාශ්රය"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Výstup AJA I/O zariadenia"
|
||||
AJAOutput.ProgramOutput="Výstup programu"
|
||||
AJAOutput.PreviewOutput="Výstup náhľadu"
|
||||
AJAOutput.MiscOutput="Ďalšie nastavenia"
|
||||
AJAOutput.MultiViewEnable="Zapnúť Multi View"
|
||||
AJAOutput.MultiViewAudioSource="Multi View zdroj zvuku"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Izhod AJA vhodno-izhodne naprave"
|
||||
AJAOutput.ProgramOutput="Programski izhod"
|
||||
AJAOutput.PreviewOutput="Izhod za predogled"
|
||||
AJAOutput.MiscOutput="Dodatne nastavitve"
|
||||
AJAOutput.MultiViewEnable="Omogoči večzaslonski pogled"
|
||||
AJAOutput.MultiViewAudioSource="Vir zvoka za večzaslonski pogled"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="I/O-enhetsutmatning för AJA"
|
||||
AJAOutput.ProgramOutput="Programutmatning"
|
||||
AJAOutput.PreviewOutput="Förhandsvisningsutmatning"
|
||||
AJAOutput.MiscOutput="Ytterligare inställningar"
|
||||
AJAOutput.MultiViewEnable="Aktivera multivy"
|
||||
AJAOutput.MultiViewAudioSource="Ljudkälla i multivy"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="เอาต์พุตอุปกรณ์ I/O ของ AJA"
|
||||
AJAOutput.ProgramOutput="เอาต์พุตของโปรแกรม"
|
||||
AJAOutput.PreviewOutput="เอาต์พุตแสดงตัวอย่าง"
|
||||
AJAOutput.MiscOutput="การตั้งค่าเพิ่มเติม"
|
||||
AJAOutput.MultiViewEnable="เปิดใช้การแสดงผลหลายจอ"
|
||||
AJAOutput.MultiViewAudioSource="มุมมองหลายจอ แหล่งเสียง"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Output ng Device na AJA I/O"
|
||||
AJAOutput.ProgramOutput="Output sa Program"
|
||||
AJAOutput.PreviewOutput="Pagpakita ng Output"
|
||||
AJAOutput.MiscOutput="Karagdagang mga Setting"
|
||||
AJAOutput.MultiViewEnable="I-enable ang Multi View"
|
||||
AJAOutput.MultiViewAudioSource="Pagkukuhanan ng Audio sa Multi View"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O Aygıt Çıktısı"
|
||||
AJAOutput.ProgramOutput="Program Çıktısı"
|
||||
AJAOutput.PreviewOutput="Önizleme Çıktısı"
|
||||
AJAOutput.MiscOutput="Ek Ayarlar"
|
||||
AJAOutput.MultiViewEnable="Çoklu Görünümü Etkinleştir"
|
||||
AJAOutput.MultiViewAudioSource="Çoklu Görünümlü Ses Kaynağı"
|
||||
@@ -0,0 +1 @@
|
||||
AJAOutput.MiscOutput="Өстәмә көйләүләр"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O ئۈسكۈنە چىقىرىش"
|
||||
AJAOutput.ProgramOutput="پىروگرامما چىقىرىش"
|
||||
AJAOutput.PreviewOutput="ئالدىن كۆزىتىش چىقىرىش"
|
||||
AJAOutput.MiscOutput="قوشۇمچە تەڭشەكلەر"
|
||||
AJAOutput.MultiViewEnable="كۆپ كۆرۈنۈشنى قوزغات"
|
||||
AJAOutput.MultiViewAudioSource="كۆپ كۆرۈنۈش ئۈن مەنبەسى"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O Пристрій виводу"
|
||||
AJAOutput.ProgramOutput="Вивід програми"
|
||||
AJAOutput.PreviewOutput="Попередній перегляд виводу"
|
||||
AJAOutput.MiscOutput="Додаткові налаштування"
|
||||
AJAOutput.MultiViewEnable="Увімкнути мульти-перегляд"
|
||||
AJAOutput.MultiViewAudioSource="Мульти-перегляд джерела звуку"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O ڈیوائس آؤٹ پٹ"
|
||||
AJAOutput.ProgramOutput="پروگرام آؤٹ پٹ"
|
||||
AJAOutput.PreviewOutput="پیش نظارہ آؤٹ پٹ"
|
||||
AJAOutput.MiscOutput="اضافی ترتیبات"
|
||||
AJAOutput.MultiViewEnable="ملٹی ویو کو فعال کریں"
|
||||
AJAOutput.MultiViewAudioSource="ملٹی ویو آڈیو ماخذ"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="Đầu ra thiết bị I/O AJA"
|
||||
AJAOutput.ProgramOutput="Đầu ra thiết bị"
|
||||
AJAOutput.PreviewOutput="Đầu ra xem trước"
|
||||
AJAOutput.MiscOutput="Thiết đặt bổ sung"
|
||||
AJAOutput.MultiViewEnable="Bật Nhiều góc nhìn"
|
||||
AJAOutput.MultiViewAudioSource="Nguồn âm thanh Nhiều góc nhìn"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O 设备输出"
|
||||
AJAOutput.ProgramOutput="程序输出"
|
||||
AJAOutput.PreviewOutput="预览输出"
|
||||
AJAOutput.MiscOutput="附加设置"
|
||||
AJAOutput.MultiViewEnable="开启多视图"
|
||||
AJAOutput.MultiViewAudioSource="音频源多视图"
|
||||
@@ -0,0 +1,6 @@
|
||||
AJAOutput.Device="AJA I/O 裝置輸出"
|
||||
AJAOutput.ProgramOutput="節目輸出"
|
||||
AJAOutput.PreviewOutput="預覽輸出"
|
||||
AJAOutput.MiscOutput="其他設定"
|
||||
AJAOutput.MultiViewEnable="啟用多檢視模式"
|
||||
AJAOutput.MultiViewAudioSource="多檢視音訊來源"
|
||||
@@ -0,0 +1,123 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Output</class>
|
||||
<widget class="QDialog" name="Output">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>540</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>AJAOutput.Device</string>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>AJAOutput.ProgramOutput</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="propertiesLayout"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="outputButton">
|
||||
<property name="text">
|
||||
<string>Start</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>AJAOutput.PreviewOutput</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="previewPropertiesLayout"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="previewOutputButton">
|
||||
<property name="text">
|
||||
<string>Start</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>AJAOutput.MiscOutput</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="miscPropertiesLayout"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,50 @@
|
||||
cmake_minimum_required(VERSION 3.28...3.30)
|
||||
|
||||
if(NOT ENABLE_DECKLINK)
|
||||
target_disable(decklink-captions)
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(OS_LINUX OR OS_FREEBSD OR OS_OPENBSD)
|
||||
find_package(X11 REQUIRED)
|
||||
mark_as_advanced(X11)
|
||||
endif()
|
||||
|
||||
find_package(Qt6 REQUIRED Widgets)
|
||||
|
||||
add_library(decklink-captions MODULE)
|
||||
add_library(OBS::decklink-captions ALIAS decklink-captions)
|
||||
|
||||
target_sources(decklink-captions PRIVATE decklink-captions.cpp decklink-captions.h forms/captions.ui)
|
||||
|
||||
target_compile_options(
|
||||
decklink-captions
|
||||
PRIVATE $<$<PLATFORM_ID:Darwin>:-Wno-quoted-include-in-framework-header> $<$<PLATFORM_ID:Darwin>:-Wno-comma>
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
decklink-captions
|
||||
PRIVATE
|
||||
OBS::frontend-api
|
||||
OBS::libobs
|
||||
Qt::Widgets
|
||||
"$<$<PLATFORM_ID:Darwin>:$<LINK_LIBRARY:FRAMEWORK,Cocoa.framework>>"
|
||||
$<$<PLATFORM_ID:Linux,FreeBSD>:X11::X11>
|
||||
)
|
||||
|
||||
if(OS_WINDOWS)
|
||||
configure_file(cmake/windows/obs-module.rc.in decklink-captions.rc)
|
||||
target_sources(decklink-captions PRIVATE decklink-captions.rc)
|
||||
|
||||
set_property(TARGET decklink-captions APPEND PROPERTY AUTORCC_OPTIONS --format-version 1)
|
||||
endif()
|
||||
|
||||
set_target_properties_obs(
|
||||
decklink-captions
|
||||
PROPERTIES FOLDER plugins/decklink
|
||||
PREFIX ""
|
||||
AUTOMOC ON
|
||||
AUTOUIC ON
|
||||
AUTORCC ON
|
||||
AUTOUIC_SEARCH_PATHS forms
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
1 VERSIONINFO
|
||||
FILEVERSION ${OBS_VERSION_MAJOR},${OBS_VERSION_MINOR},${OBS_VERSION_PATCH},0
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904B0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "${OBS_COMPANY_NAME}"
|
||||
VALUE "FileDescription", "OBS DeckLink Captions module"
|
||||
VALUE "FileVersion", "${OBS_VERSION_CANONICAL}"
|
||||
VALUE "ProductName", "${OBS_PRODUCT_NAME}"
|
||||
VALUE "ProductVersion", "${OBS_VERSION_CANONICAL}"
|
||||
VALUE "Comments", "${OBS_COMMENTS}"
|
||||
VALUE "LegalCopyright", "${OBS_LEGAL_COPYRIGHT}"
|
||||
VALUE "InternalName", "decklink-captions"
|
||||
VALUE "OriginalFilename", "decklink-captions"
|
||||
END
|
||||
END
|
||||
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x0409, 0x04B0
|
||||
END
|
||||
END
|
||||
@@ -0,0 +1,150 @@
|
||||
#include <obs-frontend-api.h>
|
||||
#include <QMainWindow>
|
||||
#include <QAction>
|
||||
#include <obs.hpp>
|
||||
#include "decklink-captions.h"
|
||||
|
||||
OBS_DECLARE_MODULE()
|
||||
OBS_MODULE_USE_DEFAULT_LOCALE("decklink-captons", "en-US")
|
||||
|
||||
struct obs_captions {
|
||||
std::string source_name;
|
||||
OBSWeakSource source;
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
obs_captions();
|
||||
inline ~obs_captions() { stop(); }
|
||||
};
|
||||
|
||||
obs_captions::obs_captions() {}
|
||||
|
||||
static obs_captions *captions = nullptr;
|
||||
|
||||
DecklinkCaptionsUI::DecklinkCaptionsUI(QWidget *parent) : QDialog(parent), ui(new Ui_CaptionsDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
setSizeGripEnabled(true);
|
||||
|
||||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||
|
||||
auto cb = [this](obs_source_t *source) {
|
||||
uint32_t caps = obs_source_get_output_flags(source);
|
||||
QString name = obs_source_get_name(source);
|
||||
|
||||
if (caps & OBS_SOURCE_CEA_708)
|
||||
ui->source->addItem(name);
|
||||
|
||||
OBSWeakSource weak = OBSGetWeakRef(source);
|
||||
if (weak == captions->source)
|
||||
ui->source->setCurrentText(name);
|
||||
return true;
|
||||
};
|
||||
|
||||
using cb_t = decltype(cb);
|
||||
|
||||
ui->source->blockSignals(true);
|
||||
ui->source->addItem(QStringLiteral(""));
|
||||
ui->source->setCurrentIndex(0);
|
||||
obs_enum_sources([](void *data, obs_source_t *source) { return (*static_cast<cb_t *>(data))(source); }, &cb);
|
||||
ui->source->blockSignals(false);
|
||||
}
|
||||
|
||||
void DecklinkCaptionsUI::on_source_currentIndexChanged(int)
|
||||
{
|
||||
captions->stop();
|
||||
|
||||
captions->source_name = ui->source->currentText().toUtf8().constData();
|
||||
captions->source = GetWeakSourceByName(captions->source_name.c_str());
|
||||
|
||||
captions->start();
|
||||
}
|
||||
|
||||
static void caption_callback(void * /* param */, obs_source_t * /* source */, const struct obs_source_cea_708 *captions)
|
||||
{
|
||||
obs_output *output = obs_frontend_get_streaming_output();
|
||||
if (output) {
|
||||
if (obs_frontend_streaming_active() && obs_output_active(output)) {
|
||||
obs_output_caption(output, captions);
|
||||
}
|
||||
obs_output_release(output);
|
||||
}
|
||||
}
|
||||
|
||||
void obs_captions::start()
|
||||
{
|
||||
OBSSource s = OBSGetStrongRef(source);
|
||||
if (!s) {
|
||||
//warn("Source invalid");
|
||||
return;
|
||||
}
|
||||
obs_source_add_caption_callback(s, caption_callback, nullptr);
|
||||
}
|
||||
|
||||
void obs_captions::stop()
|
||||
{
|
||||
OBSSource s = OBSGetStrongRef(source);
|
||||
if (s)
|
||||
obs_source_remove_caption_callback(s, caption_callback, nullptr);
|
||||
}
|
||||
|
||||
static void save_decklink_caption_data(obs_data_t *save_data, bool saving, void *)
|
||||
{
|
||||
if (saving) {
|
||||
obs_data_t *obj = obs_data_create();
|
||||
|
||||
obs_data_set_string(obj, "source", captions->source_name.c_str());
|
||||
|
||||
obs_data_set_obj(save_data, "decklink_captions", obj);
|
||||
obs_data_release(obj);
|
||||
} else {
|
||||
captions->stop();
|
||||
|
||||
obs_data_t *obj = obs_data_get_obj(save_data, "decklink_captions");
|
||||
if (!obj)
|
||||
obj = obs_data_create();
|
||||
|
||||
captions->source_name = obs_data_get_string(obj, "source");
|
||||
captions->source = GetWeakSourceByName(captions->source_name.c_str());
|
||||
obs_data_release(obj);
|
||||
|
||||
captions->start();
|
||||
}
|
||||
}
|
||||
|
||||
void addOutputUI(void)
|
||||
{
|
||||
QAction *action = (QAction *)obs_frontend_add_tools_menu_qaction(obs_module_text("Decklink Captions"));
|
||||
|
||||
captions = new obs_captions;
|
||||
|
||||
auto cb = []() {
|
||||
obs_frontend_push_ui_translation(obs_module_get_string);
|
||||
|
||||
QWidget *window = (QWidget *)obs_frontend_get_main_window();
|
||||
|
||||
DecklinkCaptionsUI dialog(window);
|
||||
dialog.exec();
|
||||
|
||||
obs_frontend_pop_ui_translation();
|
||||
};
|
||||
|
||||
obs_frontend_add_save_callback(save_decklink_caption_data, nullptr);
|
||||
|
||||
action->connect(action, &QAction::triggered, cb);
|
||||
}
|
||||
|
||||
bool obs_module_load(void)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void obs_module_post_load(void)
|
||||
{
|
||||
if (!obs_get_module("decklink"))
|
||||
return;
|
||||
|
||||
addOutputUI();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#include <QDialog>
|
||||
#include <obs-module.h>
|
||||
#include <util/platform.h>
|
||||
#include <obs.hpp>
|
||||
#include <memory>
|
||||
#include "ui_captions.h"
|
||||
|
||||
class DecklinkCaptionsUI : public QDialog {
|
||||
Q_OBJECT
|
||||
private:
|
||||
public:
|
||||
std::unique_ptr<Ui_CaptionsDialog> ui;
|
||||
DecklinkCaptionsUI(QWidget *parent);
|
||||
|
||||
public slots:
|
||||
void on_source_currentIndexChanged(int idx);
|
||||
};
|
||||
|
||||
static inline OBSWeakSource GetWeakSourceByName(const char *name)
|
||||
{
|
||||
OBSWeakSource weak;
|
||||
obs_source_t *source = obs_get_source_by_name(name);
|
||||
if (source) {
|
||||
weak = obs_source_get_weak_source(source);
|
||||
obs_weak_source_release(weak);
|
||||
obs_source_release(source);
|
||||
}
|
||||
|
||||
return weak;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>CaptionsDialog</class>
|
||||
<widget class="QDialog" name="CaptionsDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>519</width>
|
||||
<height>104</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Captions</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<property name="labelAlignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Captions.Source</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>source</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="source">
|
||||
<property name="insertPolicy">
|
||||
<enum>QComboBox::InsertAlphabetically</enum>
|
||||
</property>
|
||||
<property name="sizeAdjustPolicy">
|
||||
<enum>QComboBox::AdjustToContents</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="accept">
|
||||
<property name="text">
|
||||
<string>OK</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>accept</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>CaptionsDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>268</x>
|
||||
<y>331</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>229</x>
|
||||
<y>-11</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -0,0 +1,59 @@
|
||||
cmake_minimum_required(VERSION 3.28...3.30)
|
||||
|
||||
if(NOT ENABLE_DECKLINK)
|
||||
target_disable(decklink-output-ui)
|
||||
return()
|
||||
endif()
|
||||
|
||||
find_package(Qt6 REQUIRED Widgets)
|
||||
|
||||
if(OS_LINUX OR OS_FREEBSD OR OS_OPENBSD)
|
||||
find_package(Qt6 REQUIRED Gui)
|
||||
|
||||
find_package(X11 REQUIRED)
|
||||
endif()
|
||||
|
||||
add_library(decklink-output-ui MODULE)
|
||||
add_library(OBS::decklink-output-ui ALIAS decklink-output-ui)
|
||||
|
||||
if(NOT TARGET OBS::properties-view)
|
||||
add_subdirectory("${CMAKE_SOURCE_DIR}/shared/properties-view" "${CMAKE_BINARY_DIR}/shared/properties-view")
|
||||
endif()
|
||||
|
||||
target_sources(decklink-output-ui PRIVATE forms/output.ui)
|
||||
|
||||
target_sources(
|
||||
decklink-output-ui
|
||||
PRIVATE DecklinkOutputUI.cpp DecklinkOutputUI.h decklink-ui-main.cpp decklink-ui-main.h
|
||||
)
|
||||
|
||||
target_compile_options(decklink-output-ui PRIVATE $<$<PLATFORM_ID:Darwin>:-Wno-quoted-include-in-framework-header>)
|
||||
|
||||
target_link_libraries(
|
||||
decklink-output-ui
|
||||
PRIVATE
|
||||
OBS::libobs
|
||||
OBS::frontend-api
|
||||
OBS::properties-view
|
||||
Qt::Widgets
|
||||
"$<$<PLATFORM_ID:Darwin>:$<LINK_LIBRARY:FRAMEWORK,Cocoa.framework>>"
|
||||
$<$<PLATFORM_ID:Linux,FreeBSD,OpenBSD>:X11::X11>
|
||||
$<$<PLATFORM_ID:Linux,FreeBSD,OpenBSD>:Qt::GuiPrivate>
|
||||
)
|
||||
|
||||
if(OS_WINDOWS)
|
||||
configure_file(cmake/windows/obs-module.rc.in decklink-output-ui.rc)
|
||||
target_sources(decklink-output-ui PRIVATE decklink-output-ui.rc)
|
||||
|
||||
set_property(TARGET decklink-output-ui APPEND PROPERTY AUTORCC_OPTIONS --format-version 1)
|
||||
endif()
|
||||
|
||||
set_target_properties_obs(
|
||||
decklink-output-ui
|
||||
PROPERTIES FOLDER frontend
|
||||
PREFIX ""
|
||||
AUTOMOC ON
|
||||
AUTOUIC ON
|
||||
AUTORCC ON
|
||||
AUTOUIC_SEARCH_PATHS forms
|
||||
)
|
||||
@@ -0,0 +1,139 @@
|
||||
#include "DecklinkOutputUI.h"
|
||||
#include <obs-module.h>
|
||||
#include <util/platform.h>
|
||||
#include <util/util.hpp>
|
||||
#include "decklink-ui-main.h"
|
||||
|
||||
DecklinkOutputUI::DecklinkOutputUI(QWidget *parent) : QDialog(parent), ui(new Ui_Output)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
setSizeGripEnabled(true);
|
||||
|
||||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||
|
||||
propertiesView = nullptr;
|
||||
previewPropertiesView = nullptr;
|
||||
}
|
||||
|
||||
void DecklinkOutputUI::ShowHideDialog()
|
||||
{
|
||||
SetupPropertiesView();
|
||||
SetupPreviewPropertiesView();
|
||||
|
||||
setVisible(!isVisible());
|
||||
}
|
||||
|
||||
void DecklinkOutputUI::SetupPropertiesView()
|
||||
{
|
||||
if (propertiesView)
|
||||
delete propertiesView;
|
||||
|
||||
obs_data_t *settings = obs_data_create();
|
||||
|
||||
OBSData data = load_settings();
|
||||
if (data)
|
||||
obs_data_apply(settings, data);
|
||||
|
||||
propertiesView = new OBSPropertiesView(settings, "decklink_output",
|
||||
(PropertiesReloadCallback)obs_get_output_properties, 170);
|
||||
|
||||
ui->propertiesLayout->addWidget(propertiesView);
|
||||
obs_data_release(settings);
|
||||
|
||||
connect(propertiesView, &OBSPropertiesView::Changed, this, &DecklinkOutputUI::PropertiesChanged);
|
||||
}
|
||||
|
||||
void DecklinkOutputUI::SaveSettings()
|
||||
{
|
||||
BPtr<char> modulePath = obs_module_get_config_path(obs_current_module(), "");
|
||||
|
||||
os_mkdirs(modulePath);
|
||||
|
||||
BPtr<char> path = obs_module_get_config_path(obs_current_module(), "decklinkOutputProps.json");
|
||||
|
||||
obs_data_t *settings = propertiesView->GetSettings();
|
||||
if (settings)
|
||||
obs_data_save_json_safe(settings, path, "tmp", "bak");
|
||||
}
|
||||
|
||||
void DecklinkOutputUI::SetupPreviewPropertiesView()
|
||||
{
|
||||
if (previewPropertiesView)
|
||||
delete previewPropertiesView;
|
||||
|
||||
obs_data_t *settings = obs_data_create();
|
||||
|
||||
OBSData data = load_preview_settings();
|
||||
if (data)
|
||||
obs_data_apply(settings, data);
|
||||
|
||||
previewPropertiesView = new OBSPropertiesView(settings, "decklink_output",
|
||||
(PropertiesReloadCallback)obs_get_output_properties, 170);
|
||||
|
||||
ui->previewPropertiesLayout->addWidget(previewPropertiesView);
|
||||
obs_data_release(settings);
|
||||
|
||||
connect(previewPropertiesView, &OBSPropertiesView::Changed, this, &DecklinkOutputUI::PreviewPropertiesChanged);
|
||||
}
|
||||
|
||||
void DecklinkOutputUI::SavePreviewSettings()
|
||||
{
|
||||
BPtr<char> modulePath = obs_module_get_config_path(obs_current_module(), "");
|
||||
|
||||
os_mkdirs(modulePath);
|
||||
|
||||
BPtr<char> path = obs_module_get_config_path(obs_current_module(), "decklinkPreviewOutputProps.json");
|
||||
|
||||
obs_data_t *settings = previewPropertiesView->GetSettings();
|
||||
if (settings)
|
||||
obs_data_save_json_safe(settings, path, "tmp", "bak");
|
||||
}
|
||||
|
||||
void DecklinkOutputUI::on_outputButton_clicked()
|
||||
{
|
||||
SaveSettings();
|
||||
output_toggle();
|
||||
}
|
||||
|
||||
void DecklinkOutputUI::PropertiesChanged()
|
||||
{
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
void DecklinkOutputUI::OutputStateChanged(bool active)
|
||||
{
|
||||
QString text;
|
||||
if (active) {
|
||||
text = QString(obs_module_text("Stop"));
|
||||
} else {
|
||||
text = QString(obs_module_text("Start"));
|
||||
}
|
||||
|
||||
ui->outputButton->setChecked(active);
|
||||
ui->outputButton->setText(text);
|
||||
}
|
||||
|
||||
void DecklinkOutputUI::on_previewOutputButton_clicked()
|
||||
{
|
||||
SavePreviewSettings();
|
||||
preview_output_toggle();
|
||||
}
|
||||
|
||||
void DecklinkOutputUI::PreviewPropertiesChanged()
|
||||
{
|
||||
SavePreviewSettings();
|
||||
}
|
||||
|
||||
void DecklinkOutputUI::PreviewOutputStateChanged(bool active)
|
||||
{
|
||||
QString text;
|
||||
if (active) {
|
||||
text = QString(obs_module_text("Stop"));
|
||||
} else {
|
||||
text = QString(obs_module_text("Start"));
|
||||
}
|
||||
|
||||
ui->previewOutputButton->setChecked(active);
|
||||
ui->previewOutputButton->setText(text);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <properties-view.hpp>
|
||||
|
||||
#include "ui_output.h"
|
||||
|
||||
class DecklinkOutputUI : public QDialog {
|
||||
Q_OBJECT
|
||||
private:
|
||||
OBSPropertiesView *propertiesView;
|
||||
OBSPropertiesView *previewPropertiesView;
|
||||
|
||||
public slots:
|
||||
void on_outputButton_clicked();
|
||||
void PropertiesChanged();
|
||||
void OutputStateChanged(bool);
|
||||
|
||||
void on_previewOutputButton_clicked();
|
||||
void PreviewPropertiesChanged();
|
||||
void PreviewOutputStateChanged(bool);
|
||||
|
||||
public:
|
||||
std::unique_ptr<Ui_Output> ui;
|
||||
DecklinkOutputUI(QWidget *parent);
|
||||
|
||||
void ShowHideDialog();
|
||||
|
||||
void SetupPropertiesView();
|
||||
void SaveSettings();
|
||||
|
||||
void SetupPreviewPropertiesView();
|
||||
void SavePreviewSettings();
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
1 VERSIONINFO
|
||||
FILEVERSION ${OBS_VERSION_MAJOR},${OBS_VERSION_MINOR},${OBS_VERSION_PATCH},0
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904B0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "${OBS_COMPANY_NAME}"
|
||||
VALUE "FileDescription", "OBS Decklink Output UI"
|
||||
VALUE "FileVersion", "${OBS_VERSION_CANONICAL}"
|
||||
VALUE "ProductName", "${OBS_PRODUCT_NAME}"
|
||||
VALUE "ProductVersion", "${OBS_VERSION_CANONICAL}"
|
||||
VALUE "Comments", "${OBS_COMMENTS}"
|
||||
VALUE "LegalCopyright", "${OBS_LEGAL_COPYRIGHT}"
|
||||
VALUE "InternalName", "decklink-output-ui"
|
||||
VALUE "OriginalFilename", "decklink-output-ui"
|
||||
END
|
||||
END
|
||||
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x0409, 0x04B0
|
||||
END
|
||||
END
|
||||
@@ -0,0 +1,480 @@
|
||||
#include <obs-module.h>
|
||||
#include <obs-frontend-api.h>
|
||||
#include <QMainWindow>
|
||||
#include <QAction>
|
||||
#include <util/util.hpp>
|
||||
#include <util/platform.h>
|
||||
#include <media-io/video-io.h>
|
||||
#include <media-io/video-frame.h>
|
||||
#include "DecklinkOutputUI.h"
|
||||
#include "../../../plugins/decklink/const.h"
|
||||
|
||||
OBS_DECLARE_MODULE()
|
||||
OBS_MODULE_USE_DEFAULT_LOCALE("decklink-output-ui", "en-US")
|
||||
|
||||
DecklinkOutputUI *doUI;
|
||||
|
||||
bool shutting_down = false;
|
||||
|
||||
bool main_output_running = false;
|
||||
bool preview_output_running = false;
|
||||
|
||||
constexpr size_t STAGE_BUFFER_COUNT = 3;
|
||||
|
||||
struct decklink_ui_output {
|
||||
bool enabled;
|
||||
obs_source_t *current_source;
|
||||
obs_output_t *output;
|
||||
|
||||
video_t *video_queue;
|
||||
gs_texrender_t *texrender_premultiplied;
|
||||
gs_texrender_t *texrender;
|
||||
gs_stagesurf_t *stagesurfaces[STAGE_BUFFER_COUNT];
|
||||
bool surf_written[STAGE_BUFFER_COUNT];
|
||||
size_t stage_index;
|
||||
uint8_t *video_data;
|
||||
uint32_t video_linesize;
|
||||
|
||||
obs_video_info ovi;
|
||||
};
|
||||
|
||||
static struct decklink_ui_output context = {0};
|
||||
static struct decklink_ui_output context_preview = {0};
|
||||
|
||||
OBSData load_settings()
|
||||
{
|
||||
BPtr<char> path = obs_module_get_config_path(obs_current_module(), "decklinkOutputProps.json");
|
||||
BPtr<char> jsonData = os_quick_read_utf8_file(path);
|
||||
if (!!jsonData) {
|
||||
obs_data_t *data = obs_data_create_from_json(jsonData);
|
||||
OBSData dataRet(data);
|
||||
obs_data_release(data);
|
||||
|
||||
return dataRet;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void decklink_ui_tick(void *param, float sec);
|
||||
static void decklink_ui_render(void *param);
|
||||
|
||||
void output_stop()
|
||||
{
|
||||
obs_remove_main_rendered_callback(decklink_ui_render, &context);
|
||||
|
||||
obs_output_stop(context.output);
|
||||
obs_output_release(context.output);
|
||||
|
||||
obs_enter_graphics();
|
||||
for (gs_stagesurf_t *&surf : context.stagesurfaces) {
|
||||
gs_stagesurface_destroy(surf);
|
||||
surf = nullptr;
|
||||
}
|
||||
gs_texrender_destroy(context.texrender);
|
||||
context.texrender = nullptr;
|
||||
obs_leave_graphics();
|
||||
|
||||
video_output_close(context.video_queue);
|
||||
obs_remove_tick_callback(decklink_ui_tick, &context);
|
||||
|
||||
main_output_running = false;
|
||||
|
||||
if (!shutting_down)
|
||||
doUI->OutputStateChanged(false);
|
||||
}
|
||||
|
||||
void output_start()
|
||||
{
|
||||
OBSData settings = load_settings();
|
||||
|
||||
if (settings != nullptr) {
|
||||
obs_output_t *const output = obs_output_create("decklink_output", "decklink_output", settings, NULL);
|
||||
const struct video_scale_info *const conversion = obs_output_get_video_conversion(output);
|
||||
if (conversion != nullptr) {
|
||||
context.output = output;
|
||||
obs_add_tick_callback(decklink_ui_tick, &context);
|
||||
|
||||
obs_get_video_info(&context.ovi);
|
||||
|
||||
const uint32_t width = conversion->width;
|
||||
const uint32_t height = conversion->height;
|
||||
|
||||
obs_enter_graphics();
|
||||
context.texrender_premultiplied = nullptr;
|
||||
context.texrender = gs_texrender_create(GS_BGRA, GS_ZS_NONE);
|
||||
for (gs_stagesurf_t *&surf : context.stagesurfaces)
|
||||
surf = gs_stagesurface_create(width, height, GS_BGRA);
|
||||
obs_leave_graphics();
|
||||
|
||||
for (bool &written : context.surf_written)
|
||||
written = false;
|
||||
|
||||
context.stage_index = 0;
|
||||
|
||||
video_output_info vi = {0};
|
||||
vi.format = VIDEO_FORMAT_BGRA;
|
||||
vi.width = width;
|
||||
vi.height = height;
|
||||
vi.fps_den = context.ovi.fps_den;
|
||||
vi.fps_num = context.ovi.fps_num;
|
||||
vi.cache_size = 16;
|
||||
vi.colorspace = VIDEO_CS_DEFAULT;
|
||||
vi.range = VIDEO_RANGE_FULL;
|
||||
vi.name = "decklink_output";
|
||||
|
||||
video_output_open(&context.video_queue, &vi);
|
||||
|
||||
context.current_source = nullptr;
|
||||
obs_add_main_rendered_callback(decklink_ui_render, &context);
|
||||
|
||||
obs_output_set_media(context.output, context.video_queue, obs_get_audio());
|
||||
bool started = obs_output_start(context.output);
|
||||
|
||||
main_output_running = started;
|
||||
|
||||
if (!shutting_down)
|
||||
doUI->OutputStateChanged(started);
|
||||
|
||||
if (!started)
|
||||
output_stop();
|
||||
} else {
|
||||
obs_output_release(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void output_toggle()
|
||||
{
|
||||
if (main_output_running)
|
||||
output_stop();
|
||||
else
|
||||
output_start();
|
||||
}
|
||||
|
||||
OBSData load_preview_settings()
|
||||
{
|
||||
BPtr<char> path = obs_module_get_config_path(obs_current_module(), "decklinkPreviewOutputProps.json");
|
||||
BPtr<char> jsonData = os_quick_read_utf8_file(path);
|
||||
if (!!jsonData) {
|
||||
obs_data_t *data = obs_data_create_from_json(jsonData);
|
||||
OBSData dataRet(data);
|
||||
obs_data_release(data);
|
||||
|
||||
return dataRet;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void on_preview_scene_changed(enum obs_frontend_event event, void *param);
|
||||
|
||||
static void decklink_ui_tick(void *param, float /* sec */)
|
||||
{
|
||||
auto ctx = (struct decklink_ui_output *)param;
|
||||
|
||||
if (ctx->texrender_premultiplied)
|
||||
gs_texrender_reset(ctx->texrender_premultiplied);
|
||||
if (ctx->texrender)
|
||||
gs_texrender_reset(ctx->texrender);
|
||||
}
|
||||
|
||||
void preview_output_stop()
|
||||
{
|
||||
obs_remove_main_rendered_callback(decklink_ui_render, &context_preview);
|
||||
obs_frontend_remove_event_callback(on_preview_scene_changed, &context_preview);
|
||||
|
||||
obs_output_stop(context_preview.output);
|
||||
obs_output_release(context_preview.output);
|
||||
|
||||
obs_source_release(context_preview.current_source);
|
||||
|
||||
obs_enter_graphics();
|
||||
for (gs_stagesurf_t *&surf : context_preview.stagesurfaces) {
|
||||
gs_stagesurface_destroy(surf);
|
||||
surf = nullptr;
|
||||
}
|
||||
gs_texrender_destroy(context_preview.texrender);
|
||||
context_preview.texrender = nullptr;
|
||||
gs_texrender_destroy(context_preview.texrender_premultiplied);
|
||||
context_preview.texrender_premultiplied = nullptr;
|
||||
obs_leave_graphics();
|
||||
|
||||
video_output_close(context_preview.video_queue);
|
||||
obs_remove_tick_callback(decklink_ui_tick, &context_preview);
|
||||
|
||||
preview_output_running = false;
|
||||
|
||||
if (!shutting_down)
|
||||
doUI->PreviewOutputStateChanged(false);
|
||||
}
|
||||
|
||||
void preview_output_start()
|
||||
{
|
||||
OBSData settings = load_preview_settings();
|
||||
|
||||
if (settings != nullptr) {
|
||||
obs_output_t *const output = obs_output_create("decklink_output", "decklink_output", settings, NULL);
|
||||
const struct video_scale_info *const conversion = obs_output_get_video_conversion(output);
|
||||
if (conversion != nullptr) {
|
||||
context_preview.output = output;
|
||||
obs_add_tick_callback(decklink_ui_tick, &context_preview);
|
||||
|
||||
obs_get_video_info(&context_preview.ovi);
|
||||
|
||||
const uint32_t width = conversion->width;
|
||||
const uint32_t height = conversion->height;
|
||||
|
||||
obs_enter_graphics();
|
||||
context_preview.texrender_premultiplied = gs_texrender_create(GS_BGRA, GS_ZS_NONE);
|
||||
context_preview.texrender = gs_texrender_create(GS_BGRA, GS_ZS_NONE);
|
||||
for (gs_stagesurf_t *&surf : context_preview.stagesurfaces)
|
||||
surf = gs_stagesurface_create(width, height, GS_BGRA);
|
||||
obs_leave_graphics();
|
||||
|
||||
for (bool &written : context_preview.surf_written)
|
||||
written = false;
|
||||
|
||||
context_preview.stage_index = 0;
|
||||
|
||||
video_output_info vi = {0};
|
||||
vi.format = VIDEO_FORMAT_BGRA;
|
||||
vi.width = width;
|
||||
vi.height = height;
|
||||
vi.fps_den = context_preview.ovi.fps_den;
|
||||
vi.fps_num = context_preview.ovi.fps_num;
|
||||
vi.cache_size = 16;
|
||||
vi.colorspace = VIDEO_CS_DEFAULT;
|
||||
vi.range = VIDEO_RANGE_FULL;
|
||||
vi.name = "decklink_preview_output";
|
||||
|
||||
video_output_open(&context_preview.video_queue, &vi);
|
||||
|
||||
obs_frontend_add_event_callback(on_preview_scene_changed, &context_preview);
|
||||
if (obs_frontend_preview_program_mode_active()) {
|
||||
context_preview.current_source = obs_frontend_get_current_preview_scene();
|
||||
} else {
|
||||
context_preview.current_source = obs_frontend_get_current_scene();
|
||||
}
|
||||
obs_add_main_rendered_callback(decklink_ui_render, &context_preview);
|
||||
|
||||
obs_output_set_media(context_preview.output, context_preview.video_queue, obs_get_audio());
|
||||
bool started = obs_output_start(context_preview.output);
|
||||
|
||||
preview_output_running = started;
|
||||
if (!shutting_down)
|
||||
doUI->PreviewOutputStateChanged(started);
|
||||
|
||||
if (!started)
|
||||
preview_output_stop();
|
||||
} else {
|
||||
obs_output_release(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void preview_output_toggle()
|
||||
{
|
||||
if (preview_output_running)
|
||||
preview_output_stop();
|
||||
else
|
||||
preview_output_start();
|
||||
}
|
||||
|
||||
void on_preview_scene_changed(enum obs_frontend_event event, void *param)
|
||||
{
|
||||
auto ctx = (struct decklink_ui_output *)param;
|
||||
switch (event) {
|
||||
case OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED:
|
||||
case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED:
|
||||
obs_source_release(ctx->current_source);
|
||||
ctx->current_source = obs_frontend_get_current_preview_scene();
|
||||
break;
|
||||
case OBS_FRONTEND_EVENT_STUDIO_MODE_DISABLED:
|
||||
obs_source_release(ctx->current_source);
|
||||
ctx->current_source = obs_frontend_get_current_scene();
|
||||
break;
|
||||
case OBS_FRONTEND_EVENT_SCENE_CHANGED:
|
||||
if (!obs_frontend_preview_program_mode_active()) {
|
||||
obs_source_release(ctx->current_source);
|
||||
ctx->current_source = obs_frontend_get_current_scene();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void decklink_ui_render(void *param)
|
||||
{
|
||||
auto *const ctx = (struct decklink_ui_output *)param;
|
||||
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
gs_texture_t *tex = nullptr;
|
||||
|
||||
if (ctx == &context) {
|
||||
if (!main_output_running)
|
||||
return;
|
||||
|
||||
tex = obs_get_main_texture();
|
||||
if (!tex)
|
||||
return;
|
||||
|
||||
width = gs_texture_get_width(tex);
|
||||
height = gs_texture_get_height(tex);
|
||||
} else if (ctx == &context_preview) {
|
||||
if (!preview_output_running)
|
||||
return;
|
||||
|
||||
if (!ctx->current_source)
|
||||
return;
|
||||
|
||||
width = obs_source_get_base_width(ctx->current_source);
|
||||
height = obs_source_get_base_height(ctx->current_source);
|
||||
|
||||
gs_texrender_t *const texrender_premultiplied = ctx->texrender_premultiplied;
|
||||
if (!gs_texrender_begin(texrender_premultiplied, width, height))
|
||||
return;
|
||||
|
||||
struct vec4 background;
|
||||
vec4_zero(&background);
|
||||
|
||||
gs_clear(GS_CLEAR_COLOR, &background, 0.0f, 0);
|
||||
gs_ortho(0.0f, (float)width, 0.0f, (float)height, -100.0f, 100.0f);
|
||||
|
||||
gs_blend_state_push();
|
||||
gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO);
|
||||
|
||||
obs_source_video_render(ctx->current_source);
|
||||
|
||||
gs_blend_state_pop();
|
||||
gs_texrender_end(texrender_premultiplied);
|
||||
|
||||
tex = gs_texrender_get_texture(texrender_premultiplied);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const struct video_scale_info *const conversion = obs_output_get_video_conversion(ctx->output);
|
||||
const uint32_t scaled_width = conversion->width;
|
||||
const uint32_t scaled_height = conversion->height;
|
||||
|
||||
if (!gs_texrender_begin(ctx->texrender, scaled_width, scaled_height))
|
||||
return;
|
||||
|
||||
const bool previous = gs_framebuffer_srgb_enabled();
|
||||
const bool source_hdr = (ctx->ovi.colorspace == VIDEO_CS_2100_PQ) || (ctx->ovi.colorspace == VIDEO_CS_2100_HLG);
|
||||
const bool target_hdr = source_hdr && (conversion->colorspace == VIDEO_CS_2100_PQ);
|
||||
gs_enable_framebuffer_srgb(!target_hdr);
|
||||
gs_enable_blending(false);
|
||||
|
||||
gs_effect_t *const effect = obs_get_base_effect(OBS_EFFECT_DEFAULT);
|
||||
gs_effect_set_texture_srgb(gs_effect_get_param_by_name(effect, "image"), tex);
|
||||
const char *const tech_name = target_hdr ? "DrawAlphaDivideR10L"
|
||||
: (source_hdr ? "DrawAlphaDivideTonemap" : "DrawAlphaDivide");
|
||||
while (gs_effect_loop(effect, tech_name)) {
|
||||
gs_effect_set_float(gs_effect_get_param_by_name(effect, "multiplier"),
|
||||
obs_get_video_sdr_white_level() / 10000.f);
|
||||
gs_draw_sprite(tex, 0, 0, 0);
|
||||
}
|
||||
|
||||
gs_enable_blending(true);
|
||||
gs_enable_framebuffer_srgb(previous);
|
||||
|
||||
gs_texrender_end(ctx->texrender);
|
||||
|
||||
const size_t write_stage_index = ctx->stage_index;
|
||||
gs_stage_texture(ctx->stagesurfaces[write_stage_index], gs_texrender_get_texture(ctx->texrender));
|
||||
ctx->surf_written[write_stage_index] = true;
|
||||
|
||||
const size_t read_stage_index = (write_stage_index + 1) % STAGE_BUFFER_COUNT;
|
||||
if (ctx->surf_written[read_stage_index]) {
|
||||
struct video_frame output_frame;
|
||||
if (video_output_lock_frame(ctx->video_queue, &output_frame, 1, os_gettime_ns())) {
|
||||
gs_stagesurf_t *const read_surf = ctx->stagesurfaces[read_stage_index];
|
||||
if (gs_stagesurface_map(read_surf, &ctx->video_data, &ctx->video_linesize)) {
|
||||
uint32_t linesize = output_frame.linesize[0];
|
||||
for (uint32_t i = 0; i < scaled_height; i++) {
|
||||
uint32_t dst_offset = linesize * i;
|
||||
uint32_t src_offset = ctx->video_linesize * i;
|
||||
memcpy(output_frame.data[0] + dst_offset, ctx->video_data + src_offset,
|
||||
linesize);
|
||||
}
|
||||
|
||||
gs_stagesurface_unmap(read_surf);
|
||||
ctx->video_data = nullptr;
|
||||
}
|
||||
|
||||
video_output_unlock_frame(ctx->video_queue);
|
||||
}
|
||||
}
|
||||
|
||||
ctx->stage_index = read_stage_index;
|
||||
}
|
||||
|
||||
void addOutputUI(void)
|
||||
{
|
||||
QAction *action = (QAction *)obs_frontend_add_tools_menu_qaction(obs_module_text("Decklink Output"));
|
||||
|
||||
QMainWindow *window = (QMainWindow *)obs_frontend_get_main_window();
|
||||
|
||||
obs_frontend_push_ui_translation(obs_module_get_string);
|
||||
doUI = new DecklinkOutputUI(window);
|
||||
obs_frontend_pop_ui_translation();
|
||||
|
||||
auto cb = []() {
|
||||
doUI->ShowHideDialog();
|
||||
};
|
||||
|
||||
action->connect(action, &QAction::triggered, cb);
|
||||
}
|
||||
|
||||
static void OBSEvent(enum obs_frontend_event event, void *)
|
||||
{
|
||||
if (event == OBS_FRONTEND_EVENT_FINISHED_LOADING) {
|
||||
OBSData settings = load_settings();
|
||||
|
||||
if (settings && obs_data_get_bool(settings, "auto_start"))
|
||||
output_start();
|
||||
|
||||
OBSData previewSettings = load_preview_settings();
|
||||
|
||||
if (previewSettings && obs_data_get_bool(previewSettings, "auto_start"))
|
||||
preview_output_start();
|
||||
} else if (event == OBS_FRONTEND_EVENT_EXIT) {
|
||||
shutting_down = true;
|
||||
|
||||
if (preview_output_running)
|
||||
preview_output_stop();
|
||||
|
||||
if (main_output_running)
|
||||
output_stop();
|
||||
}
|
||||
}
|
||||
|
||||
bool obs_module_load(void)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void obs_module_unload(void)
|
||||
{
|
||||
shutting_down = true;
|
||||
|
||||
if (preview_output_running)
|
||||
preview_output_stop();
|
||||
|
||||
if (main_output_running)
|
||||
output_stop();
|
||||
}
|
||||
|
||||
void obs_module_post_load(void)
|
||||
{
|
||||
if (!obs_get_module("decklink"))
|
||||
return;
|
||||
|
||||
addOutputUI();
|
||||
|
||||
obs_frontend_add_event_callback(OBSEvent, nullptr);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
void output_toggle();
|
||||
OBSData load_settings();
|
||||
void preview_output_toggle();
|
||||
OBSData load_preview_settings();
|
||||
@@ -0,0 +1,113 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Output</class>
|
||||
<widget class="QDialog" name="Output">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>785</width>
|
||||
<height>484</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Decklink Output</string>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Output</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="propertiesLayout"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="outputButton">
|
||||
<property name="text">
|
||||
<string>Start</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Preview Output</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="previewPropertiesLayout"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="previewOutputButton">
|
||||
<property name="text">
|
||||
<string>Start</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,95 @@
|
||||
cmake_minimum_required(VERSION 3.28...3.30)
|
||||
|
||||
find_package(Qt6 REQUIRED Widgets)
|
||||
|
||||
if(OS_LINUX OR OS_FREEBSD OR OS_OPENBSD)
|
||||
find_package(Qt6 REQUIRED Gui)
|
||||
find_package(X11 REQUIRED)
|
||||
endif()
|
||||
|
||||
add_library(frontend-tools MODULE)
|
||||
add_library(OBS::frontend-tools ALIAS frontend-tools)
|
||||
|
||||
if(NOT TARGET OBS::properties-view)
|
||||
add_subdirectory("${CMAKE_SOURCE_DIR}/shared/properties-view" "${CMAKE_BINARY_DIR}/shared/properties-view")
|
||||
endif()
|
||||
|
||||
if(NOT TARGET OBS::qt-plain-text-edit)
|
||||
add_subdirectory("${CMAKE_SOURCE_DIR}/shared/qt/plain-text-edit" "${CMAKE_BINARY_DIR}/shared/qt/plain-text-edit")
|
||||
endif()
|
||||
|
||||
if(NOT TARGET OBS::qt-wrappers)
|
||||
add_subdirectory("${CMAKE_SOURCE_DIR}/shared/qt/wrappers" "${CMAKE_BINARY_DIR}/shared/qt/wrappers")
|
||||
endif()
|
||||
|
||||
target_sources(
|
||||
frontend-tools
|
||||
PRIVATE
|
||||
$<$<PLATFORM_ID:Darwin>:auto-scene-switcher-osx.mm>
|
||||
$<$<PLATFORM_ID:Linux,FreeBSD,OpenBSD>:auto-scene-switcher-nix.cpp>
|
||||
$<$<PLATFORM_ID:Windows>:auto-scene-switcher-win.cpp>
|
||||
$<$<PLATFORM_ID:Windows>:captions-handler.cpp>
|
||||
$<$<PLATFORM_ID:Windows>:captions-handler.hpp>
|
||||
$<$<PLATFORM_ID:Windows>:captions-mssapi-stream.cpp>
|
||||
$<$<PLATFORM_ID:Windows>:captions-mssapi-stream.hpp>
|
||||
$<$<PLATFORM_ID:Windows>:captions-mssapi.cpp>
|
||||
$<$<PLATFORM_ID:Windows>:captions-mssapi.hpp>
|
||||
$<$<PLATFORM_ID:Windows>:captions.cpp>
|
||||
$<$<PLATFORM_ID:Windows>:captions.hpp>
|
||||
auto-scene-switcher.cpp
|
||||
auto-scene-switcher.hpp
|
||||
frontend-tools.c
|
||||
output-timer.cpp
|
||||
output-timer.hpp
|
||||
tool-helpers.hpp
|
||||
)
|
||||
|
||||
target_sources(
|
||||
frontend-tools
|
||||
PRIVATE forms/auto-scene-switcher.ui forms/captions.ui forms/output-timer.ui forms/scripts.ui
|
||||
)
|
||||
|
||||
target_compile_options(
|
||||
frontend-tools
|
||||
PRIVATE $<$<PLATFORM_ID:Darwin>:-Wno-quoted-include-in-framework-header> $<$<PLATFORM_ID:Darwin>:-Wno-comma>
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
frontend-tools
|
||||
PRIVATE
|
||||
OBS::frontend-api
|
||||
OBS::libobs
|
||||
OBS::properties-view
|
||||
OBS::qt-plain-text-edit
|
||||
OBS::qt-wrappers
|
||||
Qt::Widgets
|
||||
"$<$<PLATFORM_ID:Darwin>:$<LINK_LIBRARY:FRAMEWORK,Cocoa>>"
|
||||
$<$<PLATFORM_ID:Linux,FreeBSD,OpenBSD>:X11::X11>
|
||||
$<$<PLATFORM_ID:Linux,FreeBSD,OpenBSD>:Qt::GuiPrivate>
|
||||
)
|
||||
|
||||
add_subdirectory("${CMAKE_SOURCE_DIR}/shared/obs-scripting" "${CMAKE_BINARY_DIR}/shared/obs-scripting")
|
||||
|
||||
if(ENABLE_SCRIPTING AND TARGET OBS::scripting)
|
||||
target_sources(frontend-tools PRIVATE scripts.cpp scripts.hpp)
|
||||
target_link_libraries(frontend-tools PRIVATE OBS::scripting)
|
||||
target_enable_feature(frontend-tools "Scripting Support (Frontend)" ENABLE_SCRIPTING)
|
||||
endif()
|
||||
|
||||
if(OS_WINDOWS)
|
||||
configure_file(cmake/windows/obs-module.rc.in frontend-tools.rc)
|
||||
target_sources(frontend-tools PRIVATE frontend-tools.rc)
|
||||
|
||||
set_property(TARGET frontend-tools APPEND PROPERTY AUTORCC_OPTIONS --format-version 1)
|
||||
endif()
|
||||
|
||||
set_target_properties_obs(
|
||||
frontend-tools
|
||||
PROPERTIES FOLDER frontend
|
||||
PREFIX ""
|
||||
AUTOMOC ON
|
||||
AUTOUIC ON
|
||||
AUTORCC ON
|
||||
AUTOUIC_SEARCH_PATHS forms
|
||||
XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC YES
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/Xutil.h>
|
||||
#undef Bool
|
||||
#undef CursorShape
|
||||
#undef Expose
|
||||
#undef KeyPress
|
||||
#undef KeyRelease
|
||||
#undef FocusIn
|
||||
#undef FocusOut
|
||||
#undef FontChange
|
||||
#undef None
|
||||
#undef Status
|
||||
#undef Unsorted
|
||||
#include <util/platform.h>
|
||||
#include "auto-scene-switcher.hpp"
|
||||
|
||||
using namespace std;
|
||||
|
||||
static Display *xdisplay = 0;
|
||||
|
||||
Display *disp()
|
||||
{
|
||||
if (!xdisplay)
|
||||
xdisplay = XOpenDisplay(NULL);
|
||||
|
||||
return xdisplay;
|
||||
}
|
||||
|
||||
void CleanupSceneSwitcher()
|
||||
{
|
||||
if (!xdisplay)
|
||||
return;
|
||||
|
||||
XCloseDisplay(xdisplay);
|
||||
xdisplay = 0;
|
||||
}
|
||||
|
||||
static bool ewmhIsSupported()
|
||||
{
|
||||
Display *display = disp();
|
||||
Atom netSupportingWmCheck = XInternAtom(display, "_NET_SUPPORTING_WM_CHECK", true);
|
||||
Atom actualType;
|
||||
int format = 0;
|
||||
unsigned long num = 0, bytes = 0;
|
||||
unsigned char *data = NULL;
|
||||
Window ewmh_window = 0;
|
||||
Window root_window = 0;
|
||||
|
||||
root_window = DefaultRootWindow(display);
|
||||
if (!root_window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int status = XGetWindowProperty(display, root_window, netSupportingWmCheck, 0L, 1L, false, XA_WINDOW,
|
||||
&actualType, &format, &num, &bytes, &data);
|
||||
|
||||
if (status == Success) {
|
||||
if (num > 0) {
|
||||
ewmh_window = ((Window *)data)[0];
|
||||
}
|
||||
if (data) {
|
||||
XFree(data);
|
||||
data = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (ewmh_window) {
|
||||
status = XGetWindowProperty(display, ewmh_window, netSupportingWmCheck, 0L, 1L, false, XA_WINDOW,
|
||||
&actualType, &format, &num, &bytes, &data);
|
||||
if (status != Success || num == 0 || ewmh_window != ((Window *)data)[0]) {
|
||||
ewmh_window = 0;
|
||||
}
|
||||
if (status == Success && data) {
|
||||
XFree(data);
|
||||
}
|
||||
}
|
||||
|
||||
return ewmh_window != 0;
|
||||
}
|
||||
|
||||
static std::vector<Window> getTopLevelWindows()
|
||||
{
|
||||
std::vector<Window> res;
|
||||
|
||||
res.resize(0);
|
||||
|
||||
if (!ewmhIsSupported()) {
|
||||
return res;
|
||||
}
|
||||
|
||||
Atom netClList = XInternAtom(disp(), "_NET_CLIENT_LIST", true);
|
||||
Atom actualType;
|
||||
int format;
|
||||
unsigned long num, bytes;
|
||||
Window *data = 0;
|
||||
|
||||
for (int i = 0; i < ScreenCount(disp()); ++i) {
|
||||
Window rootWin = RootWindow(disp(), i);
|
||||
if (!rootWin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int status = XGetWindowProperty(disp(), rootWin, netClList, 0L, ~0L, false, AnyPropertyType,
|
||||
&actualType, &format, &num, &bytes, (uint8_t **)&data);
|
||||
|
||||
if (status != Success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (unsigned long i = 0; i < num; ++i)
|
||||
res.emplace_back(data[i]);
|
||||
|
||||
XFree(data);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static std::string GetWindowTitle(size_t i)
|
||||
{
|
||||
Window w = getTopLevelWindows().at(i);
|
||||
if (!w) {
|
||||
return "";
|
||||
}
|
||||
std::string windowTitle;
|
||||
char *name;
|
||||
|
||||
int status = XFetchName(disp(), w, &name);
|
||||
if (status >= Success && name != nullptr) {
|
||||
std::string str(name);
|
||||
windowTitle = str;
|
||||
XFree(name);
|
||||
} else {
|
||||
XTextProperty xtp_new_name;
|
||||
if (XGetWMName(disp(), w, &xtp_new_name) != 0 && xtp_new_name.value != nullptr) {
|
||||
std::string str((const char *)xtp_new_name.value);
|
||||
windowTitle = str;
|
||||
XFree(xtp_new_name.value);
|
||||
}
|
||||
}
|
||||
|
||||
return windowTitle;
|
||||
}
|
||||
|
||||
void GetWindowList(vector<string> &windows)
|
||||
{
|
||||
windows.resize(0);
|
||||
|
||||
for (size_t i = 0; i < getTopLevelWindows().size(); ++i) {
|
||||
if (GetWindowTitle(i) != "")
|
||||
windows.emplace_back(GetWindowTitle(i));
|
||||
}
|
||||
}
|
||||
|
||||
void GetCurrentWindowTitle(string &title)
|
||||
{
|
||||
if (!ewmhIsSupported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Atom active = XInternAtom(disp(), "_NET_ACTIVE_WINDOW", true);
|
||||
Atom actualType;
|
||||
int format;
|
||||
unsigned long num, bytes;
|
||||
Window *data = 0;
|
||||
char *name;
|
||||
|
||||
Window rootWin = RootWindow(disp(), 0);
|
||||
if (!rootWin) {
|
||||
return;
|
||||
}
|
||||
|
||||
XGetWindowProperty(disp(), rootWin, active, 0L, ~0L, false, AnyPropertyType, &actualType, &format, &num, &bytes,
|
||||
(uint8_t **)&data);
|
||||
|
||||
if (!data[0]) {
|
||||
return;
|
||||
}
|
||||
int status = XFetchName(disp(), data[0], &name);
|
||||
|
||||
if (status >= Success && name != nullptr) {
|
||||
std::string str(name);
|
||||
title = str;
|
||||
} else {
|
||||
XTextProperty xtp_new_name;
|
||||
if (XGetWMName(disp(), data[0], &xtp_new_name) != 0 && xtp_new_name.value != nullptr) {
|
||||
std::string str((const char *)xtp_new_name.value);
|
||||
title = str;
|
||||
XFree(xtp_new_name.value);
|
||||
}
|
||||
}
|
||||
|
||||
XFree(name);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
#include <util/platform.h>
|
||||
#include "auto-scene-switcher.hpp"
|
||||
|
||||
using namespace std;
|
||||
|
||||
void GetWindowList(vector<string> &windows)
|
||||
{
|
||||
windows.resize(0);
|
||||
|
||||
@autoreleasepool {
|
||||
NSWorkspace *ws = [NSWorkspace sharedWorkspace];
|
||||
NSArray *array = [ws runningApplications];
|
||||
for (NSRunningApplication *app in array) {
|
||||
NSString *name = app.localizedName;
|
||||
if (!name)
|
||||
continue;
|
||||
|
||||
const char *str = name.UTF8String;
|
||||
if (str && *str)
|
||||
windows.emplace_back(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GetCurrentWindowTitle(string &title)
|
||||
{
|
||||
title.resize(0);
|
||||
|
||||
@autoreleasepool {
|
||||
NSWorkspace *ws = [NSWorkspace sharedWorkspace];
|
||||
NSRunningApplication *app = [ws frontmostApplication];
|
||||
if (app) {
|
||||
NSString *name = app.localizedName;
|
||||
if (!name)
|
||||
return;
|
||||
|
||||
const char *str = name.UTF8String;
|
||||
if (str && *str)
|
||||
title = str;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CleanupSceneSwitcher() {}
|
||||
@@ -0,0 +1,71 @@
|
||||
#include <windows.h>
|
||||
#include <util/platform.h>
|
||||
#include "auto-scene-switcher.hpp"
|
||||
|
||||
using namespace std;
|
||||
|
||||
static bool GetWindowTitle(HWND window, string &title)
|
||||
{
|
||||
size_t len = (size_t)GetWindowTextLengthW(window);
|
||||
wstring wtitle;
|
||||
|
||||
wtitle.resize(len);
|
||||
if (!GetWindowTextW(window, &wtitle[0], (int)len + 1))
|
||||
return false;
|
||||
|
||||
len = os_wcs_to_utf8(wtitle.c_str(), 0, nullptr, 0);
|
||||
title.resize(len);
|
||||
os_wcs_to_utf8(wtitle.c_str(), 0, &title[0], len + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool WindowValid(HWND window)
|
||||
{
|
||||
LONG_PTR styles, ex_styles;
|
||||
RECT rect;
|
||||
DWORD id;
|
||||
|
||||
if (!IsWindowVisible(window))
|
||||
return false;
|
||||
GetWindowThreadProcessId(window, &id);
|
||||
if (id == GetCurrentProcessId())
|
||||
return false;
|
||||
|
||||
GetClientRect(window, &rect);
|
||||
styles = GetWindowLongPtr(window, GWL_STYLE);
|
||||
ex_styles = GetWindowLongPtr(window, GWL_EXSTYLE);
|
||||
|
||||
if (ex_styles & WS_EX_TOOLWINDOW)
|
||||
return false;
|
||||
if (styles & WS_CHILD)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void GetWindowList(vector<string> &windows)
|
||||
{
|
||||
HWND window = GetWindow(GetDesktopWindow(), GW_CHILD);
|
||||
|
||||
while (window) {
|
||||
string title;
|
||||
if (WindowValid(window) && GetWindowTitle(window, title))
|
||||
windows.emplace_back(title);
|
||||
window = GetNextWindow(window, GW_HWNDNEXT);
|
||||
}
|
||||
}
|
||||
|
||||
void GetCurrentWindowTitle(string &title)
|
||||
{
|
||||
HWND window = GetForegroundWindow();
|
||||
DWORD id;
|
||||
|
||||
GetWindowThreadProcessId(window, &id);
|
||||
if (id == GetCurrentProcessId()) {
|
||||
title = "";
|
||||
return;
|
||||
}
|
||||
GetWindowTitle(window, title);
|
||||
}
|
||||
|
||||
void CleanupSceneSwitcher() {}
|
||||
@@ -0,0 +1,507 @@
|
||||
#include <obs-frontend-api.h>
|
||||
#include <obs-module.h>
|
||||
#include <obs.hpp>
|
||||
#include <util/util.hpp>
|
||||
#include <QMainWindow>
|
||||
#include <QMessageBox>
|
||||
#include <QAction>
|
||||
#include "auto-scene-switcher.hpp"
|
||||
#include "tool-helpers.hpp"
|
||||
|
||||
#include <condition_variable>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include <regex>
|
||||
#include <mutex>
|
||||
|
||||
using namespace std;
|
||||
|
||||
#define DEFAULT_INTERVAL 300
|
||||
|
||||
struct SceneSwitch {
|
||||
OBSWeakSource scene;
|
||||
string window;
|
||||
regex re;
|
||||
|
||||
inline SceneSwitch(OBSWeakSource scene_, const char *window_) : scene(scene_), window(window_), re(window_) {}
|
||||
};
|
||||
|
||||
static inline bool WeakSourceValid(obs_weak_source_t *ws)
|
||||
{
|
||||
OBSSourceAutoRelease source = obs_weak_source_get_source(ws);
|
||||
return !!source;
|
||||
}
|
||||
|
||||
struct SwitcherData {
|
||||
thread th;
|
||||
condition_variable cv;
|
||||
mutex m;
|
||||
bool stop = false;
|
||||
|
||||
vector<SceneSwitch> switches;
|
||||
OBSWeakSource nonMatchingScene;
|
||||
int interval = DEFAULT_INTERVAL;
|
||||
bool switchIfNotMatching = false;
|
||||
|
||||
void Thread();
|
||||
void Start();
|
||||
void Stop();
|
||||
|
||||
void Prune()
|
||||
{
|
||||
for (size_t i = 0; i < switches.size(); i++) {
|
||||
SceneSwitch &s = switches[i];
|
||||
if (!WeakSourceValid(s.scene))
|
||||
switches.erase(switches.begin() + i--);
|
||||
}
|
||||
|
||||
if (nonMatchingScene && !WeakSourceValid(nonMatchingScene)) {
|
||||
switchIfNotMatching = false;
|
||||
nonMatchingScene = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
inline ~SwitcherData() { Stop(); }
|
||||
};
|
||||
|
||||
static SwitcherData *switcher = nullptr;
|
||||
|
||||
static inline QString MakeSwitchName(const QString &scene, const QString &window)
|
||||
{
|
||||
return QStringLiteral("[") + scene + QStringLiteral("]: ") + window;
|
||||
}
|
||||
|
||||
SceneSwitcher::SceneSwitcher(QWidget *parent) : QDialog(parent), ui(new Ui_SceneSwitcher)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||
|
||||
lock_guard<mutex> lock(switcher->m);
|
||||
|
||||
switcher->Prune();
|
||||
|
||||
BPtr<char *> scenes = obs_frontend_get_scene_names();
|
||||
char **temp = scenes;
|
||||
while (*temp) {
|
||||
const char *name = *temp;
|
||||
ui->scenes->addItem(name);
|
||||
ui->noMatchSwitchScene->addItem(name);
|
||||
temp++;
|
||||
}
|
||||
|
||||
if (switcher->switchIfNotMatching)
|
||||
ui->noMatchSwitch->setChecked(true);
|
||||
else
|
||||
ui->noMatchDontSwitch->setChecked(true);
|
||||
|
||||
ui->noMatchSwitchScene->setCurrentText(GetWeakSourceName(switcher->nonMatchingScene).c_str());
|
||||
ui->checkInterval->setValue(switcher->interval);
|
||||
|
||||
vector<string> windows;
|
||||
GetWindowList(windows);
|
||||
|
||||
for (string &window : windows)
|
||||
ui->windows->addItem(window.c_str());
|
||||
|
||||
for (auto &s : switcher->switches) {
|
||||
string sceneName = GetWeakSourceName(s.scene);
|
||||
QString text = MakeSwitchName(sceneName.c_str(), s.window.c_str());
|
||||
|
||||
QListWidgetItem *item = new QListWidgetItem(text, ui->switches);
|
||||
item->setData(Qt::UserRole, s.window.c_str());
|
||||
}
|
||||
|
||||
if (switcher->th.joinable())
|
||||
SetStarted();
|
||||
else
|
||||
SetStopped();
|
||||
|
||||
loading = false;
|
||||
connect(this, &QDialog::finished, this, &SceneSwitcher::finished);
|
||||
}
|
||||
|
||||
void SceneSwitcher::finished()
|
||||
{
|
||||
obs_frontend_save();
|
||||
}
|
||||
|
||||
int SceneSwitcher::FindByData(const QString &window)
|
||||
{
|
||||
int count = ui->switches->count();
|
||||
int idx = -1;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
QListWidgetItem *item = ui->switches->item(i);
|
||||
QString itemWindow = item->data(Qt::UserRole).toString();
|
||||
|
||||
if (itemWindow == window) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return idx;
|
||||
}
|
||||
|
||||
void SceneSwitcher::on_switches_currentRowChanged(int idx)
|
||||
{
|
||||
if (loading)
|
||||
return;
|
||||
if (idx == -1)
|
||||
return;
|
||||
|
||||
QListWidgetItem *item = ui->switches->item(idx);
|
||||
|
||||
QString window = item->data(Qt::UserRole).toString();
|
||||
|
||||
lock_guard<mutex> lock(switcher->m);
|
||||
for (auto &s : switcher->switches) {
|
||||
if (window.compare(s.window.c_str()) == 0) {
|
||||
string name = GetWeakSourceName(s.scene);
|
||||
ui->scenes->setCurrentText(name.c_str());
|
||||
ui->windows->setCurrentText(window);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SceneSwitcher::on_close_clicked()
|
||||
{
|
||||
done(0);
|
||||
}
|
||||
|
||||
void SceneSwitcher::on_add_clicked()
|
||||
{
|
||||
QString sceneName = ui->scenes->currentText();
|
||||
QString windowName = ui->windows->currentText();
|
||||
|
||||
if (windowName.isEmpty())
|
||||
return;
|
||||
|
||||
OBSWeakSource source = GetWeakSourceByQString(sceneName);
|
||||
QVariant v = QVariant::fromValue(windowName);
|
||||
|
||||
QString text = MakeSwitchName(sceneName, windowName);
|
||||
|
||||
int idx = FindByData(windowName);
|
||||
|
||||
if (idx == -1) {
|
||||
try {
|
||||
lock_guard<mutex> lock(switcher->m);
|
||||
switcher->switches.emplace_back(source, windowName.toUtf8().constData());
|
||||
|
||||
QListWidgetItem *item = new QListWidgetItem(text, ui->switches);
|
||||
item->setData(Qt::UserRole, v);
|
||||
} catch (const regex_error &) {
|
||||
QMessageBox::warning(this, obs_module_text("InvalidRegex.Title"),
|
||||
obs_module_text("InvalidRegex.Text"));
|
||||
}
|
||||
} else {
|
||||
QListWidgetItem *item = ui->switches->item(idx);
|
||||
item->setText(text);
|
||||
|
||||
string window = windowName.toUtf8().constData();
|
||||
|
||||
{
|
||||
lock_guard<mutex> lock(switcher->m);
|
||||
for (auto &s : switcher->switches) {
|
||||
if (s.window == window) {
|
||||
s.scene = source;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui->switches->sortItems();
|
||||
}
|
||||
}
|
||||
|
||||
void SceneSwitcher::on_remove_clicked()
|
||||
{
|
||||
QListWidgetItem *item = ui->switches->currentItem();
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
string window = item->data(Qt::UserRole).toString().toUtf8().constData();
|
||||
|
||||
{
|
||||
lock_guard<mutex> lock(switcher->m);
|
||||
auto &switches = switcher->switches;
|
||||
|
||||
for (auto it = switches.begin(); it != switches.end(); ++it) {
|
||||
auto &s = *it;
|
||||
|
||||
if (s.window == window) {
|
||||
switches.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete item;
|
||||
}
|
||||
|
||||
void SceneSwitcher::UpdateNonMatchingScene(const QString &name)
|
||||
{
|
||||
OBSSourceAutoRelease scene = obs_get_source_by_name(name.toUtf8().constData());
|
||||
OBSWeakSourceAutoRelease ws = obs_source_get_weak_source(scene);
|
||||
|
||||
switcher->nonMatchingScene = ws.Get();
|
||||
}
|
||||
|
||||
void SceneSwitcher::on_noMatchDontSwitch_clicked()
|
||||
{
|
||||
if (loading)
|
||||
return;
|
||||
|
||||
lock_guard<mutex> lock(switcher->m);
|
||||
switcher->switchIfNotMatching = false;
|
||||
}
|
||||
|
||||
void SceneSwitcher::on_noMatchSwitch_clicked()
|
||||
{
|
||||
if (loading)
|
||||
return;
|
||||
|
||||
lock_guard<mutex> lock(switcher->m);
|
||||
switcher->switchIfNotMatching = true;
|
||||
UpdateNonMatchingScene(ui->noMatchSwitchScene->currentText());
|
||||
}
|
||||
|
||||
void SceneSwitcher::on_noMatchSwitchScene_currentTextChanged(const QString &text)
|
||||
{
|
||||
if (loading)
|
||||
return;
|
||||
|
||||
lock_guard<mutex> lock(switcher->m);
|
||||
UpdateNonMatchingScene(text);
|
||||
}
|
||||
|
||||
void SceneSwitcher::on_checkInterval_valueChanged(int value)
|
||||
{
|
||||
if (loading)
|
||||
return;
|
||||
|
||||
lock_guard<mutex> lock(switcher->m);
|
||||
switcher->interval = value;
|
||||
}
|
||||
|
||||
void SceneSwitcher::SetStarted()
|
||||
{
|
||||
ui->toggleStartButton->setText(obs_module_text("Stop"));
|
||||
ui->pluginRunningText->setText(obs_module_text("Active"));
|
||||
}
|
||||
|
||||
void SceneSwitcher::SetStopped()
|
||||
{
|
||||
ui->toggleStartButton->setText(obs_module_text("Start"));
|
||||
ui->pluginRunningText->setText(obs_module_text("Inactive"));
|
||||
}
|
||||
|
||||
void SceneSwitcher::on_toggleStartButton_clicked()
|
||||
{
|
||||
if (switcher->th.joinable()) {
|
||||
switcher->Stop();
|
||||
SetStopped();
|
||||
} else {
|
||||
switcher->Start();
|
||||
SetStarted();
|
||||
}
|
||||
}
|
||||
|
||||
static void SaveSceneSwitcher(obs_data_t *save_data, bool saving, void *)
|
||||
{
|
||||
if (saving) {
|
||||
lock_guard<mutex> lock(switcher->m);
|
||||
OBSDataAutoRelease obj = obs_data_create();
|
||||
OBSDataArrayAutoRelease array = obs_data_array_create();
|
||||
|
||||
switcher->Prune();
|
||||
|
||||
for (SceneSwitch &s : switcher->switches) {
|
||||
OBSDataAutoRelease array_obj = obs_data_create();
|
||||
|
||||
OBSSourceAutoRelease source = obs_weak_source_get_source(s.scene);
|
||||
if (source) {
|
||||
const char *n = obs_source_get_name(source);
|
||||
obs_data_set_string(array_obj, "scene", n);
|
||||
obs_data_set_string(array_obj, "window_title", s.window.c_str());
|
||||
obs_data_array_push_back(array, array_obj);
|
||||
}
|
||||
}
|
||||
|
||||
string nonMatchingSceneName = GetWeakSourceName(switcher->nonMatchingScene);
|
||||
|
||||
obs_data_set_int(obj, "interval", switcher->interval);
|
||||
obs_data_set_string(obj, "non_matching_scene", nonMatchingSceneName.c_str());
|
||||
obs_data_set_bool(obj, "switch_if_not_matching", switcher->switchIfNotMatching);
|
||||
obs_data_set_bool(obj, "active", switcher->th.joinable());
|
||||
obs_data_set_array(obj, "switches", array);
|
||||
|
||||
obs_data_set_obj(save_data, "auto-scene-switcher", obj);
|
||||
} else {
|
||||
switcher->m.lock();
|
||||
|
||||
OBSDataAutoRelease obj = obs_data_get_obj(save_data, "auto-scene-switcher");
|
||||
OBSDataArrayAutoRelease array = obs_data_get_array(obj, "switches");
|
||||
size_t count = obs_data_array_count(array);
|
||||
|
||||
if (!obj)
|
||||
obj = obs_data_create();
|
||||
|
||||
obs_data_set_default_int(obj, "interval", DEFAULT_INTERVAL);
|
||||
|
||||
switcher->interval = obs_data_get_int(obj, "interval");
|
||||
switcher->switchIfNotMatching = obs_data_get_bool(obj, "switch_if_not_matching");
|
||||
string nonMatchingScene = obs_data_get_string(obj, "non_matching_scene");
|
||||
bool active = obs_data_get_bool(obj, "active");
|
||||
|
||||
switcher->nonMatchingScene = GetWeakSourceByName(nonMatchingScene.c_str());
|
||||
|
||||
switcher->switches.clear();
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
OBSDataAutoRelease array_obj = obs_data_array_item(array, i);
|
||||
|
||||
const char *scene = obs_data_get_string(array_obj, "scene");
|
||||
const char *window = obs_data_get_string(array_obj, "window_title");
|
||||
|
||||
switcher->switches.emplace_back(GetWeakSourceByName(scene), window);
|
||||
}
|
||||
|
||||
switcher->m.unlock();
|
||||
|
||||
if (active)
|
||||
switcher->Start();
|
||||
else
|
||||
switcher->Stop();
|
||||
}
|
||||
}
|
||||
|
||||
void SwitcherData::Thread()
|
||||
{
|
||||
chrono::duration<long long, milli> duration = chrono::milliseconds(interval);
|
||||
string lastTitle;
|
||||
string title;
|
||||
|
||||
for (;;) {
|
||||
unique_lock<mutex> lock(m);
|
||||
OBSWeakSource scene;
|
||||
bool match = false;
|
||||
|
||||
cv.wait_for(lock, duration);
|
||||
if (switcher->stop) {
|
||||
switcher->stop = false;
|
||||
break;
|
||||
}
|
||||
|
||||
duration = chrono::milliseconds(interval);
|
||||
|
||||
GetCurrentWindowTitle(title);
|
||||
|
||||
if (lastTitle != title) {
|
||||
switcher->Prune();
|
||||
|
||||
for (SceneSwitch &s : switches) {
|
||||
if (s.window == title) {
|
||||
match = true;
|
||||
scene = s.scene;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* try regex */
|
||||
if (!match) {
|
||||
for (SceneSwitch &s : switches) {
|
||||
try {
|
||||
bool matches = regex_match(title, s.re);
|
||||
if (matches) {
|
||||
match = true;
|
||||
scene = s.scene;
|
||||
break;
|
||||
}
|
||||
} catch (const regex_error &) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!match && switchIfNotMatching && nonMatchingScene) {
|
||||
match = true;
|
||||
scene = nonMatchingScene;
|
||||
}
|
||||
|
||||
if (match) {
|
||||
OBSSourceAutoRelease source = obs_weak_source_get_source(scene);
|
||||
OBSSourceAutoRelease currentSource = obs_frontend_get_current_scene();
|
||||
|
||||
if (source && source != currentSource)
|
||||
obs_frontend_set_current_scene(source);
|
||||
}
|
||||
}
|
||||
|
||||
lastTitle = title;
|
||||
}
|
||||
}
|
||||
|
||||
void SwitcherData::Start()
|
||||
{
|
||||
if (!switcher->th.joinable())
|
||||
switcher->th = thread([]() { switcher->Thread(); });
|
||||
}
|
||||
|
||||
void SwitcherData::Stop()
|
||||
{
|
||||
if (th.joinable()) {
|
||||
{
|
||||
lock_guard<mutex> lock(m);
|
||||
stop = true;
|
||||
}
|
||||
cv.notify_one();
|
||||
th.join();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void FreeSceneSwitcher()
|
||||
{
|
||||
CleanupSceneSwitcher();
|
||||
|
||||
delete switcher;
|
||||
switcher = nullptr;
|
||||
}
|
||||
|
||||
static void OBSEvent(enum obs_frontend_event event, void *)
|
||||
{
|
||||
if (event == OBS_FRONTEND_EVENT_EXIT)
|
||||
FreeSceneSwitcher();
|
||||
}
|
||||
|
||||
extern "C" void InitSceneSwitcher()
|
||||
{
|
||||
#if !defined(__APPLE__) && !defined(_WIN32)
|
||||
if (QApplication::platformName().contains("wayland"))
|
||||
return;
|
||||
#endif
|
||||
|
||||
QAction *action = (QAction *)obs_frontend_add_tools_menu_qaction(obs_module_text("SceneSwitcher"));
|
||||
|
||||
switcher = new SwitcherData;
|
||||
|
||||
auto cb = []() {
|
||||
obs_frontend_push_ui_translation(obs_module_get_string);
|
||||
|
||||
QMainWindow *window = (QMainWindow *)obs_frontend_get_main_window();
|
||||
|
||||
SceneSwitcher ss(window);
|
||||
ss.exec();
|
||||
|
||||
obs_frontend_pop_ui_translation();
|
||||
};
|
||||
|
||||
obs_frontend_add_save_callback(SaveSceneSwitcher, nullptr);
|
||||
obs_frontend_add_event_callback(OBSEvent, nullptr);
|
||||
|
||||
action->connect(action, &QAction::triggered, cb);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include "ui_auto-scene-switcher.h"
|
||||
|
||||
struct obs_weak_source;
|
||||
typedef struct obs_weak_source obs_weak_source_t;
|
||||
|
||||
class QCloseEvent;
|
||||
|
||||
class SceneSwitcher : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
std::unique_ptr<Ui_SceneSwitcher> ui;
|
||||
bool loading = true;
|
||||
|
||||
SceneSwitcher(QWidget *parent);
|
||||
|
||||
void SetStarted();
|
||||
void SetStopped();
|
||||
|
||||
int FindByData(const QString &window);
|
||||
|
||||
void UpdateNonMatchingScene(const QString &name);
|
||||
|
||||
public slots:
|
||||
void on_switches_currentRowChanged(int idx);
|
||||
void on_close_clicked();
|
||||
void on_add_clicked();
|
||||
void on_remove_clicked();
|
||||
void on_noMatchDontSwitch_clicked();
|
||||
void on_noMatchSwitch_clicked();
|
||||
void on_noMatchSwitchScene_currentTextChanged(const QString &text);
|
||||
void on_checkInterval_valueChanged(int value);
|
||||
void on_toggleStartButton_clicked();
|
||||
void finished();
|
||||
};
|
||||
|
||||
void GetWindowList(std::vector<std::string> &windows);
|
||||
void GetCurrentWindowTitle(std::string &title);
|
||||
void CleanupSceneSwitcher();
|
||||
@@ -0,0 +1,39 @@
|
||||
#include "captions-handler.hpp"
|
||||
|
||||
captions_handler::captions_handler(captions_cb callback, enum audio_format format, uint32_t sample_rate) : cb(callback)
|
||||
{
|
||||
if (!reset_resampler(format, sample_rate))
|
||||
throw CAPTIONS_ERROR_GENERIC_FAIL;
|
||||
}
|
||||
|
||||
bool captions_handler::reset_resampler(enum audio_format format, uint32_t sample_rate)
|
||||
try {
|
||||
obs_audio_info ai;
|
||||
if (!obs_get_audio_info(&ai))
|
||||
throw std::string("Failed to get OBS audio info");
|
||||
|
||||
resample_info src = {ai.samples_per_sec, AUDIO_FORMAT_FLOAT_PLANAR, ai.speakers};
|
||||
resample_info dst = {sample_rate, format, SPEAKERS_MONO};
|
||||
|
||||
if (!resampler.reset(dst, src))
|
||||
throw std::string("Failed to create audio resampler");
|
||||
|
||||
return true;
|
||||
|
||||
} catch (std::string text) {
|
||||
blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
void captions_handler::push_audio(const audio_data *audio)
|
||||
{
|
||||
uint8_t *out[MAX_AV_PLANES];
|
||||
uint32_t frames;
|
||||
uint64_t ts_offset;
|
||||
bool success;
|
||||
|
||||
success = audio_resampler_resample(resampler, out, &frames, &ts_offset, (const uint8_t *const *)audio->data,
|
||||
audio->frames);
|
||||
if (success)
|
||||
pcm_data(out[0], frames);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <media-io/audio-resampler.h>
|
||||
#include <obs-module.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
class resampler_obj {
|
||||
audio_resampler_t *resampler = nullptr;
|
||||
|
||||
public:
|
||||
inline ~resampler_obj() { audio_resampler_destroy(resampler); }
|
||||
|
||||
inline bool reset(const resample_info &dst, const resample_info &src)
|
||||
{
|
||||
audio_resampler_destroy(resampler);
|
||||
resampler = audio_resampler_create(&dst, &src);
|
||||
return !!resampler;
|
||||
}
|
||||
|
||||
inline operator audio_resampler_t *() { return resampler; }
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
typedef std::function<void(const std::string &)> captions_cb;
|
||||
|
||||
#define CAPTIONS_ERROR_GENERIC_FAIL std::string(obs_module_text("Captions.Error.GenericFail"))
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
class captions_handler {
|
||||
captions_cb cb;
|
||||
resampler_obj resampler;
|
||||
|
||||
protected:
|
||||
inline void callback(const std::string &text) { cb(text); }
|
||||
|
||||
virtual void pcm_data(const void *data, size_t frames) = 0;
|
||||
|
||||
/* always resamples to 1 channel */
|
||||
bool reset_resampler(enum audio_format format, uint32_t sample_rate);
|
||||
|
||||
public:
|
||||
/* throw std::string for errors shown to users */
|
||||
captions_handler(captions_cb callback, enum audio_format format, uint32_t sample_rate);
|
||||
virtual ~captions_handler() {}
|
||||
|
||||
void push_audio(const audio_data *audio);
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
struct captions_handler_info {
|
||||
std::string (*name)(void);
|
||||
captions_handler *(*create)(captions_cb cb, const std::string &lang);
|
||||
};
|
||||
@@ -0,0 +1,388 @@
|
||||
#include "captions-mssapi-stream.hpp"
|
||||
#include "captions-mssapi.hpp"
|
||||
#include <mmreg.h>
|
||||
#include <util/windows/CoTaskMemPtr.hpp>
|
||||
#include <util/threading.h>
|
||||
#include <util/base.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
#if 0
|
||||
#define debugfunc(format, ...) blog(LOG_DEBUG, "[Captions] %s(" format ")", __FUNCTION__, ##__VA_ARGS__)
|
||||
#else
|
||||
#define debugfunc(format, ...)
|
||||
#endif
|
||||
|
||||
CaptionStream::CaptionStream(DWORD samplerate_, mssapi_captions *handler_)
|
||||
: handler(handler_),
|
||||
samplerate(samplerate_),
|
||||
event(CreateEvent(nullptr, false, false, nullptr))
|
||||
{
|
||||
buf_info.ulMsMinNotification = 50;
|
||||
buf_info.ulMsBufferSize = 500;
|
||||
buf_info.ulMsEventBias = 0;
|
||||
|
||||
format.wFormatTag = WAVE_FORMAT_PCM;
|
||||
format.nChannels = 1;
|
||||
format.nSamplesPerSec = 16000;
|
||||
format.nAvgBytesPerSec = format.nSamplesPerSec * sizeof(uint16_t);
|
||||
format.nBlockAlign = 2;
|
||||
format.wBitsPerSample = 16;
|
||||
format.cbSize = sizeof(format);
|
||||
}
|
||||
|
||||
void CaptionStream::Stop()
|
||||
{
|
||||
{
|
||||
lock_guard<mutex> lock(m);
|
||||
deque_free(buf);
|
||||
}
|
||||
|
||||
cv.notify_one();
|
||||
}
|
||||
|
||||
void CaptionStream::PushAudio(const void *data, size_t frames)
|
||||
{
|
||||
bool ready = false;
|
||||
|
||||
lock_guard<mutex> lock(m);
|
||||
deque_push_back(buf, data, frames * sizeof(int16_t));
|
||||
write_pos += frames * sizeof(int16_t);
|
||||
|
||||
if (wait_size && buf->size >= wait_size)
|
||||
ready = true;
|
||||
if (ready)
|
||||
cv.notify_one();
|
||||
}
|
||||
|
||||
// IUnknown methods
|
||||
|
||||
STDMETHODIMP CaptionStream::QueryInterface(REFIID riid, void **ppv)
|
||||
{
|
||||
if (riid == IID_IUnknown) {
|
||||
AddRef();
|
||||
*ppv = this;
|
||||
|
||||
} else if (riid == IID_IStream) {
|
||||
AddRef();
|
||||
*ppv = (IStream *)this;
|
||||
|
||||
} else if (riid == IID_ISpStreamFormat) {
|
||||
AddRef();
|
||||
*ppv = (ISpStreamFormat *)this;
|
||||
|
||||
} else if (riid == IID_ISpAudio) {
|
||||
AddRef();
|
||||
*ppv = (ISpAudio *)this;
|
||||
|
||||
} else {
|
||||
*ppv = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
return NOERROR;
|
||||
}
|
||||
|
||||
STDMETHODIMP_(ULONG) CaptionStream::AddRef()
|
||||
{
|
||||
return (ULONG)os_atomic_inc_long(&refs);
|
||||
}
|
||||
|
||||
STDMETHODIMP_(ULONG) CaptionStream::Release()
|
||||
{
|
||||
ULONG new_refs = (ULONG)os_atomic_dec_long(&refs);
|
||||
if (!new_refs)
|
||||
delete this;
|
||||
|
||||
return new_refs;
|
||||
}
|
||||
|
||||
// ISequentialStream methods
|
||||
|
||||
STDMETHODIMP CaptionStream::Read(void *data, ULONG bytes, ULONG *read_bytes)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
size_t cur_size;
|
||||
|
||||
debugfunc("data, %lu, read_bytes", bytes);
|
||||
if (!data)
|
||||
return STG_E_INVALIDPOINTER;
|
||||
|
||||
{
|
||||
lock_guard<mutex> lock1(m);
|
||||
wait_size = bytes;
|
||||
cur_size = buf->size;
|
||||
}
|
||||
|
||||
unique_lock<mutex> lock(m);
|
||||
|
||||
if (bytes > cur_size)
|
||||
cv.wait(lock);
|
||||
|
||||
if (bytes > (ULONG)buf->size) {
|
||||
bytes = (ULONG)buf->size;
|
||||
hr = S_FALSE;
|
||||
}
|
||||
if (bytes)
|
||||
deque_pop_front(buf, data, bytes);
|
||||
if (read_bytes)
|
||||
*read_bytes = bytes;
|
||||
|
||||
wait_size = 0;
|
||||
pos.QuadPart += bytes;
|
||||
return hr;
|
||||
}
|
||||
|
||||
STDMETHODIMP CaptionStream::Write(const void *, ULONG bytes, ULONG *)
|
||||
{
|
||||
debugfunc("data, %lu, written_bytes", bytes);
|
||||
|
||||
return STG_E_INVALIDFUNCTION;
|
||||
}
|
||||
|
||||
// IStream methods
|
||||
|
||||
STDMETHODIMP CaptionStream::Seek(LARGE_INTEGER move, DWORD origin, ULARGE_INTEGER *new_pos)
|
||||
{
|
||||
debugfunc("%lld, %lx, new_pos", move, origin);
|
||||
|
||||
if (!new_pos)
|
||||
return E_POINTER;
|
||||
|
||||
if (origin != SEEK_CUR || move.QuadPart != 0)
|
||||
return E_NOTIMPL;
|
||||
|
||||
*new_pos = pos;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDMETHODIMP CaptionStream::SetSize(ULARGE_INTEGER new_size)
|
||||
{
|
||||
debugfunc("%llu", new_size);
|
||||
return STG_E_INVALIDFUNCTION;
|
||||
}
|
||||
|
||||
STDMETHODIMP CaptionStream::CopyTo(IStream *stream, ULARGE_INTEGER bytes, ULARGE_INTEGER *read_bytes,
|
||||
ULARGE_INTEGER *written_bytes)
|
||||
{
|
||||
HRESULT hr;
|
||||
|
||||
debugfunc("stream, %llu, read_bytes, written_bytes", bytes);
|
||||
|
||||
if (!stream)
|
||||
return STG_E_INVALIDPOINTER;
|
||||
|
||||
ULONG written = 0;
|
||||
if (bytes.QuadPart > (ULONGLONG)buf->size)
|
||||
bytes.QuadPart = (ULONGLONG)buf->size;
|
||||
|
||||
lock_guard<mutex> lock(m);
|
||||
temp_buf.resize((size_t)bytes.QuadPart);
|
||||
deque_peek_front(buf, &temp_buf[0], (size_t)bytes.QuadPart);
|
||||
|
||||
hr = stream->Write(temp_buf.data(), (ULONG)bytes.QuadPart, &written);
|
||||
|
||||
if (read_bytes)
|
||||
*read_bytes = bytes;
|
||||
if (written_bytes)
|
||||
written_bytes->QuadPart = written;
|
||||
|
||||
return hr;
|
||||
}
|
||||
|
||||
STDMETHODIMP CaptionStream::Commit(DWORD commit_flags)
|
||||
{
|
||||
debugfunc("%lx", commit_flags);
|
||||
/* TODO? */
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDMETHODIMP CaptionStream::Revert(void)
|
||||
{
|
||||
debugfunc("");
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDMETHODIMP CaptionStream::LockRegion(ULARGE_INTEGER offset, ULARGE_INTEGER size, DWORD type)
|
||||
{
|
||||
debugfunc("%llu, %llu, %ld", offset, size, type);
|
||||
/* TODO? */
|
||||
return STG_E_INVALIDFUNCTION;
|
||||
}
|
||||
|
||||
STDMETHODIMP CaptionStream::UnlockRegion(ULARGE_INTEGER offset, ULARGE_INTEGER size, DWORD type)
|
||||
{
|
||||
debugfunc("%llu, %llu, %ld", offset, size, type);
|
||||
/* TODO? */
|
||||
return STG_E_INVALIDFUNCTION;
|
||||
}
|
||||
|
||||
static const wchar_t *stat_name = L"Caption stream";
|
||||
|
||||
STDMETHODIMP CaptionStream::Stat(STATSTG *stg, DWORD flag)
|
||||
{
|
||||
debugfunc("stg, %lu", flag);
|
||||
|
||||
if (!stg)
|
||||
return E_POINTER;
|
||||
|
||||
lock_guard<mutex> lock(m);
|
||||
*stg = {};
|
||||
stg->type = STGTY_STREAM;
|
||||
stg->cbSize.QuadPart = (ULONGLONG)buf->size;
|
||||
|
||||
if (flag == STATFLAG_DEFAULT) {
|
||||
size_t byte_size = (wcslen(stat_name) + 1) * sizeof(wchar_t);
|
||||
stg->pwcsName = (wchar_t *)CoTaskMemAlloc(byte_size);
|
||||
memcpy(stg->pwcsName, stat_name, byte_size);
|
||||
}
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDMETHODIMP CaptionStream::Clone(IStream **stream)
|
||||
{
|
||||
debugfunc("stream");
|
||||
*stream = nullptr;
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
// ISpStreamFormat methods
|
||||
|
||||
STDMETHODIMP CaptionStream::GetFormat(GUID *guid, WAVEFORMATEX **co_mem_wfex_out)
|
||||
{
|
||||
debugfunc("guid, co_mem_wfex_out");
|
||||
|
||||
if (!guid || !co_mem_wfex_out)
|
||||
return E_POINTER;
|
||||
|
||||
if (format.wFormatTag == 0) {
|
||||
*co_mem_wfex_out = nullptr;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
void *wfex = CoTaskMemAlloc(sizeof(format));
|
||||
memcpy(wfex, &format, sizeof(format));
|
||||
|
||||
*co_mem_wfex_out = (WAVEFORMATEX *)wfex;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// ISpAudio methods
|
||||
|
||||
STDMETHODIMP CaptionStream::SetState(SPAUDIOSTATE state_, ULONGLONG)
|
||||
{
|
||||
debugfunc("%lu, reserved", state_);
|
||||
state = state_;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDMETHODIMP CaptionStream::SetFormat(REFGUID guid_ref, const WAVEFORMATEX *wfex)
|
||||
{
|
||||
debugfunc("guid, wfex");
|
||||
if (!wfex)
|
||||
return E_INVALIDARG;
|
||||
|
||||
if (guid_ref == SPDFID_WaveFormatEx) {
|
||||
lock_guard<mutex> lock(m);
|
||||
memcpy(&format, wfex, sizeof(format));
|
||||
if (!handler->reset_resampler(AUDIO_FORMAT_16BIT, wfex->nSamplesPerSec))
|
||||
return E_FAIL;
|
||||
|
||||
/* 50 msec */
|
||||
DWORD size = format.nSamplesPerSec / 20;
|
||||
DWORD byte_size = size * format.nBlockAlign;
|
||||
deque_reserve(buf, (size_t)byte_size);
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDMETHODIMP CaptionStream::GetStatus(SPAUDIOSTATUS *status)
|
||||
{
|
||||
debugfunc("status");
|
||||
|
||||
if (!status)
|
||||
return E_POINTER;
|
||||
|
||||
/* TODO? */
|
||||
lock_guard<mutex> lock(m);
|
||||
*status = {};
|
||||
status->cbNonBlockingIO = (ULONG)buf->size;
|
||||
status->State = state;
|
||||
status->CurSeekPos = pos.QuadPart;
|
||||
status->CurDevicePos = write_pos;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDMETHODIMP CaptionStream::SetBufferInfo(const SPAUDIOBUFFERINFO *buf_info_)
|
||||
{
|
||||
debugfunc("buf_info");
|
||||
|
||||
/* TODO */
|
||||
buf_info = *buf_info_;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDMETHODIMP CaptionStream::GetBufferInfo(SPAUDIOBUFFERINFO *buf_info_)
|
||||
{
|
||||
debugfunc("buf_info");
|
||||
if (!buf_info_)
|
||||
return E_POINTER;
|
||||
|
||||
*buf_info_ = buf_info;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDMETHODIMP CaptionStream::GetDefaultFormat(GUID *format, WAVEFORMATEX **co_mem_wfex_out)
|
||||
{
|
||||
debugfunc("format, co_mem_wfex_out");
|
||||
|
||||
if (!format || !co_mem_wfex_out)
|
||||
return E_POINTER;
|
||||
|
||||
void *wfex = CoTaskMemAlloc(sizeof(format));
|
||||
memcpy(wfex, &format, sizeof(format));
|
||||
|
||||
*format = SPDFID_WaveFormatEx;
|
||||
*co_mem_wfex_out = (WAVEFORMATEX *)wfex;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDMETHODIMP_(HANDLE) CaptionStream::EventHandle(void)
|
||||
{
|
||||
debugfunc("");
|
||||
return event;
|
||||
}
|
||||
|
||||
STDMETHODIMP CaptionStream::GetVolumeLevel(ULONG *level)
|
||||
{
|
||||
debugfunc("level");
|
||||
if (!level)
|
||||
return E_POINTER;
|
||||
|
||||
*level = vol;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDMETHODIMP CaptionStream::SetVolumeLevel(ULONG level)
|
||||
{
|
||||
debugfunc("%lu", level);
|
||||
vol = level;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDMETHODIMP CaptionStream::GetBufferNotifySize(ULONG *size)
|
||||
{
|
||||
debugfunc("size");
|
||||
if (!size)
|
||||
return E_POINTER;
|
||||
*size = notify_size;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDMETHODIMP CaptionStream::SetBufferNotifySize(ULONG size)
|
||||
{
|
||||
debugfunc("%lu", size);
|
||||
notify_size = size;
|
||||
return S_OK;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <sapi.h>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <obs.h>
|
||||
#include <util/deque.h>
|
||||
#include <util/windows/WinHandle.hpp>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
class Deque {
|
||||
deque buf = {};
|
||||
|
||||
public:
|
||||
inline ~Deque() { deque_free(&buf); }
|
||||
inline operator deque *() { return &buf; }
|
||||
inline deque *operator->() { return &buf; }
|
||||
};
|
||||
|
||||
class mssapi_captions;
|
||||
|
||||
class CaptionStream : public ISpAudio {
|
||||
volatile long refs = 1;
|
||||
SPAUDIOBUFFERINFO buf_info = {};
|
||||
mssapi_captions *handler;
|
||||
ULONG notify_size = 0;
|
||||
SPAUDIOSTATE state;
|
||||
WinHandle event;
|
||||
ULONG vol = 0;
|
||||
|
||||
std::condition_variable cv;
|
||||
std::mutex m;
|
||||
std::vector<int16_t> temp_buf;
|
||||
WAVEFORMATEX format = {};
|
||||
|
||||
Deque buf;
|
||||
ULONG wait_size = 0;
|
||||
DWORD samplerate = 0;
|
||||
ULARGE_INTEGER pos = {};
|
||||
ULONGLONG write_pos = 0;
|
||||
|
||||
public:
|
||||
CaptionStream(DWORD samplerate, mssapi_captions *handler_);
|
||||
|
||||
void Stop();
|
||||
void PushAudio(const void *data, size_t frames);
|
||||
|
||||
// IUnknown methods
|
||||
STDMETHODIMP QueryInterface(REFIID riid, void **ppv) override;
|
||||
STDMETHODIMP_(ULONG) AddRef() override;
|
||||
STDMETHODIMP_(ULONG) Release() override;
|
||||
|
||||
// ISequentialStream methods
|
||||
STDMETHODIMP Read(void *data, ULONG bytes, ULONG *read_bytes) override;
|
||||
STDMETHODIMP Write(const void *data, ULONG bytes, ULONG *written_bytes) override;
|
||||
|
||||
// IStream methods
|
||||
STDMETHODIMP Seek(LARGE_INTEGER move, DWORD origin, ULARGE_INTEGER *new_pos) override;
|
||||
STDMETHODIMP SetSize(ULARGE_INTEGER new_size) override;
|
||||
STDMETHODIMP CopyTo(IStream *stream, ULARGE_INTEGER bytes, ULARGE_INTEGER *read_bytes,
|
||||
ULARGE_INTEGER *written_bytes) override;
|
||||
STDMETHODIMP Commit(DWORD commit_flags) override;
|
||||
STDMETHODIMP Revert(void) override;
|
||||
STDMETHODIMP LockRegion(ULARGE_INTEGER offset, ULARGE_INTEGER size, DWORD type) override;
|
||||
STDMETHODIMP UnlockRegion(ULARGE_INTEGER offset, ULARGE_INTEGER size, DWORD type) override;
|
||||
STDMETHODIMP Stat(STATSTG *stg, DWORD flags) override;
|
||||
STDMETHODIMP Clone(IStream **stream) override;
|
||||
|
||||
// ISpStreamFormat methods
|
||||
STDMETHODIMP GetFormat(GUID *guid, WAVEFORMATEX **co_mem_wfex_out) override;
|
||||
|
||||
// ISpAudio methods
|
||||
STDMETHODIMP SetState(SPAUDIOSTATE state, ULONGLONG reserved) override;
|
||||
STDMETHODIMP SetFormat(REFGUID guid_ref, const WAVEFORMATEX *wfex) override;
|
||||
STDMETHODIMP GetStatus(SPAUDIOSTATUS *status) override;
|
||||
STDMETHODIMP SetBufferInfo(const SPAUDIOBUFFERINFO *buf_info) override;
|
||||
STDMETHODIMP GetBufferInfo(SPAUDIOBUFFERINFO *buf_info) override;
|
||||
STDMETHODIMP GetDefaultFormat(GUID *format, WAVEFORMATEX **co_mem_wfex_out) override;
|
||||
STDMETHODIMP_(HANDLE) EventHandle(void) override;
|
||||
STDMETHODIMP GetVolumeLevel(ULONG *level) override;
|
||||
STDMETHODIMP SetVolumeLevel(ULONG level) override;
|
||||
STDMETHODIMP GetBufferNotifySize(ULONG *size) override;
|
||||
STDMETHODIMP SetBufferNotifySize(ULONG size) override;
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
#include "captions-mssapi.hpp"
|
||||
|
||||
#define do_log(type, format, ...) blog(type, "[Captions] " format, ##__VA_ARGS__)
|
||||
|
||||
#define error(format, ...) do_log(LOG_ERROR, format, ##__VA_ARGS__)
|
||||
#define debug(format, ...) do_log(LOG_DEBUG, format, ##__VA_ARGS__)
|
||||
|
||||
mssapi_captions::mssapi_captions(captions_cb callback, const std::string &lang)
|
||||
try : captions_handler(callback, AUDIO_FORMAT_16BIT, 16000) {
|
||||
HRESULT hr;
|
||||
|
||||
std::wstring wlang;
|
||||
wlang.resize(lang.size());
|
||||
|
||||
for (size_t i = 0; i < lang.size(); i++)
|
||||
wlang[i] = (wchar_t)lang[i];
|
||||
|
||||
LCID lang_id = LocaleNameToLCID(wlang.c_str(), 0);
|
||||
|
||||
wchar_t lang_str[32];
|
||||
_snwprintf(lang_str, 31, L"language=%x", (int)lang_id);
|
||||
|
||||
stop = CreateEvent(nullptr, false, false, nullptr);
|
||||
if (!stop.Valid())
|
||||
throw "Failed to create event";
|
||||
|
||||
hr = SpFindBestToken(SPCAT_RECOGNIZERS, lang_str, nullptr, &token);
|
||||
if (FAILED(hr))
|
||||
throw HRError("SpFindBestToken failed", hr);
|
||||
|
||||
hr = CoCreateInstance(CLSID_SpInprocRecognizer, nullptr, CLSCTX_ALL, __uuidof(ISpRecognizer),
|
||||
(void **)&recognizer);
|
||||
if (FAILED(hr))
|
||||
throw HRError("CoCreateInstance for recognizer failed", hr);
|
||||
|
||||
hr = recognizer->SetRecognizer(token);
|
||||
if (FAILED(hr))
|
||||
throw HRError("SetRecognizer failed", hr);
|
||||
|
||||
hr = recognizer->SetRecoState(SPRST_INACTIVE);
|
||||
if (FAILED(hr))
|
||||
throw HRError("SetRecoState(SPRST_INACTIVE) failed", hr);
|
||||
|
||||
hr = recognizer->CreateRecoContext(&context);
|
||||
if (FAILED(hr))
|
||||
throw HRError("CreateRecoContext failed", hr);
|
||||
|
||||
ULONGLONG interest = SPFEI(SPEI_RECOGNITION) | SPFEI(SPEI_END_SR_STREAM);
|
||||
hr = context->SetInterest(interest, interest);
|
||||
if (FAILED(hr))
|
||||
throw HRError("SetInterest failed", hr);
|
||||
|
||||
hr = context->SetNotifyWin32Event();
|
||||
if (FAILED(hr))
|
||||
throw HRError("SetNotifyWin32Event", hr);
|
||||
|
||||
notify = context->GetNotifyEventHandle();
|
||||
if (notify == INVALID_HANDLE_VALUE)
|
||||
throw HRError("GetNotifyEventHandle failed", E_NOINTERFACE);
|
||||
|
||||
size_t sample_rate = audio_output_get_sample_rate(obs_get_audio());
|
||||
audio = new CaptionStream((DWORD)sample_rate, this);
|
||||
audio->Release();
|
||||
|
||||
hr = recognizer->SetInput(audio, false);
|
||||
if (FAILED(hr))
|
||||
throw HRError("SetInput failed", hr);
|
||||
|
||||
hr = context->CreateGrammar(1, &grammar);
|
||||
if (FAILED(hr))
|
||||
throw HRError("CreateGrammar failed", hr);
|
||||
|
||||
hr = grammar->LoadDictation(nullptr, SPLO_STATIC);
|
||||
if (FAILED(hr))
|
||||
throw HRError("LoadDictation failed", hr);
|
||||
|
||||
try {
|
||||
t = std::thread([this]() { main_thread(); });
|
||||
} catch (...) {
|
||||
throw "Failed to create thread";
|
||||
}
|
||||
|
||||
} catch (const char *err) {
|
||||
blog(LOG_WARNING, "%s: %s", __FUNCTION__, err);
|
||||
throw CAPTIONS_ERROR_GENERIC_FAIL;
|
||||
|
||||
} catch (HRError err) {
|
||||
blog(LOG_WARNING, "%s: %s (%lX)", __FUNCTION__, err.str, err.hr);
|
||||
throw CAPTIONS_ERROR_GENERIC_FAIL;
|
||||
}
|
||||
|
||||
mssapi_captions::~mssapi_captions()
|
||||
{
|
||||
if (t.joinable()) {
|
||||
SetEvent(stop);
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
|
||||
void mssapi_captions::main_thread()
|
||||
try {
|
||||
HRESULT hr;
|
||||
|
||||
os_set_thread_name(__FUNCTION__);
|
||||
|
||||
hr = grammar->SetDictationState(SPRS_ACTIVE);
|
||||
if (FAILED(hr))
|
||||
throw HRError("SetDictationState failed", hr);
|
||||
|
||||
hr = recognizer->SetRecoState(SPRST_ACTIVE);
|
||||
if (FAILED(hr))
|
||||
throw HRError("SetRecoState(SPRST_ACTIVE) failed", hr);
|
||||
|
||||
HANDLE events[] = {notify, stop};
|
||||
|
||||
started = true;
|
||||
|
||||
for (;;) {
|
||||
DWORD ret = WaitForMultipleObjects(2, events, false, INFINITE);
|
||||
if (ret != WAIT_OBJECT_0)
|
||||
break;
|
||||
|
||||
CSpEvent event;
|
||||
bool exit = false;
|
||||
|
||||
while (event.GetFrom(context) == S_OK) {
|
||||
if (event.eEventId == SPEI_RECOGNITION) {
|
||||
ISpRecoResult *result = event.RecoResult();
|
||||
|
||||
CoTaskMemPtr<wchar_t> text;
|
||||
hr = result->GetText((ULONG)-1, (ULONG)-1, true, &text, nullptr);
|
||||
if (FAILED(hr))
|
||||
continue;
|
||||
|
||||
char text_utf8[512];
|
||||
os_wcs_to_utf8(text, 0, text_utf8, 512);
|
||||
|
||||
callback(text_utf8);
|
||||
|
||||
blog(LOG_DEBUG, "\"%s\"", text_utf8);
|
||||
|
||||
} else if (event.eEventId == SPEI_END_SR_STREAM) {
|
||||
exit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (exit)
|
||||
break;
|
||||
}
|
||||
|
||||
audio->Stop();
|
||||
|
||||
} catch (HRError err) {
|
||||
blog(LOG_WARNING, "%s failed: %s (%lX)", __FUNCTION__, err.str, err.hr);
|
||||
}
|
||||
|
||||
void mssapi_captions::pcm_data(const void *data, size_t frames)
|
||||
{
|
||||
if (started)
|
||||
audio->PushAudio(data, frames);
|
||||
}
|
||||
|
||||
captions_handler_info mssapi_info = {[]() -> std::string { return "Microsoft Speech-to-Text"; },
|
||||
[](captions_cb cb, const std::string &lang) -> captions_handler * {
|
||||
return new mssapi_captions(cb, lang);
|
||||
}};
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "captions-handler.hpp"
|
||||
#include "captions-mssapi-stream.hpp"
|
||||
#include <util/windows/HRError.hpp>
|
||||
#include <util/windows/ComPtr.hpp>
|
||||
#include <util/windows/WinHandle.hpp>
|
||||
#include <util/windows/CoTaskMemPtr.hpp>
|
||||
#include <util/threading.h>
|
||||
#include <util/platform.h>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 4996)
|
||||
#endif
|
||||
|
||||
#include <sphelper.h>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(pop)
|
||||
#endif
|
||||
|
||||
#include <obs.hpp>
|
||||
|
||||
#include <thread>
|
||||
|
||||
class mssapi_captions : public captions_handler {
|
||||
friend class CaptionStream;
|
||||
|
||||
ComPtr<CaptionStream> audio;
|
||||
ComPtr<ISpObjectToken> token;
|
||||
ComPtr<ISpRecoGrammar> grammar;
|
||||
ComPtr<ISpRecognizer> recognizer;
|
||||
ComPtr<ISpRecoContext> context;
|
||||
|
||||
HANDLE notify;
|
||||
WinHandle stop;
|
||||
std::thread t;
|
||||
bool started = false;
|
||||
|
||||
void main_thread();
|
||||
|
||||
public:
|
||||
mssapi_captions(captions_cb callback, const std::string &lang);
|
||||
virtual ~mssapi_captions();
|
||||
virtual void pcm_data(const void *data, size_t frames) override;
|
||||
};
|
||||
@@ -0,0 +1,421 @@
|
||||
#include <QMessageBox>
|
||||
#include <QAction>
|
||||
|
||||
#include <windows.h>
|
||||
#include <obs-frontend-api.h>
|
||||
#include "captions.hpp"
|
||||
#include "captions-handler.hpp"
|
||||
#include "tool-helpers.hpp"
|
||||
#include <util/dstr.hpp>
|
||||
#include <util/platform.h>
|
||||
#include <util/windows/WinHandle.hpp>
|
||||
#include <util/windows/ComPtr.hpp>
|
||||
#include <obs-module.h>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 4996)
|
||||
#endif
|
||||
|
||||
#include <sphelper.h>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(pop)
|
||||
#endif
|
||||
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
|
||||
#include "captions-mssapi.hpp"
|
||||
|
||||
#define do_log(type, format, ...) blog(type, "[Captions] " format, ##__VA_ARGS__)
|
||||
|
||||
#define warn(format, ...) do_log(LOG_WARNING, format, ##__VA_ARGS__)
|
||||
#define debug(format, ...) do_log(LOG_DEBUG, format, ##__VA_ARGS__)
|
||||
|
||||
using namespace std;
|
||||
|
||||
#define DEFAULT_HANDLER "mssapi"
|
||||
|
||||
struct obs_captions {
|
||||
string handler_id = DEFAULT_HANDLER;
|
||||
string source_name;
|
||||
OBSWeakSource source;
|
||||
unique_ptr<captions_handler> handler;
|
||||
LANGID lang_id = GetUserDefaultUILanguage();
|
||||
|
||||
std::unordered_map<std::string, captions_handler_info &> handler_types;
|
||||
|
||||
inline void register_handler(const char *id, captions_handler_info &info) { handler_types.emplace(id, info); }
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
obs_captions();
|
||||
inline ~obs_captions() { stop(); }
|
||||
};
|
||||
|
||||
static obs_captions *captions = nullptr;
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
struct locale_info {
|
||||
DStr name;
|
||||
LANGID id;
|
||||
|
||||
inline locale_info() {}
|
||||
inline locale_info(const locale_info &) = delete;
|
||||
inline locale_info(locale_info &&li) : name(std::move(li.name)), id(li.id) {}
|
||||
};
|
||||
|
||||
static void get_valid_locale_names(vector<locale_info> &names);
|
||||
static bool valid_lang(LANGID id);
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
CaptionsDialog::CaptionsDialog(QWidget *parent) : QDialog(parent), ui(new Ui_CaptionsDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||
|
||||
auto cb = [this](obs_source_t *source) {
|
||||
uint32_t caps = obs_source_get_output_flags(source);
|
||||
QString name = obs_source_get_name(source);
|
||||
|
||||
if (caps & OBS_SOURCE_AUDIO)
|
||||
ui->source->addItem(name);
|
||||
|
||||
OBSWeakSource weak = OBSGetWeakRef(source);
|
||||
if (weak == captions->source)
|
||||
ui->source->setCurrentText(name);
|
||||
return true;
|
||||
};
|
||||
|
||||
using cb_t = decltype(cb);
|
||||
|
||||
ui->source->blockSignals(true);
|
||||
ui->source->addItem(QStringLiteral(""));
|
||||
ui->source->setCurrentIndex(0);
|
||||
obs_enum_sources([](void *data, obs_source_t *source) { return (*static_cast<cb_t *>(data))(source); }, &cb);
|
||||
ui->source->blockSignals(false);
|
||||
|
||||
for (auto &ht : captions->handler_types) {
|
||||
QString name = ht.second.name().c_str();
|
||||
QString id = ht.first.c_str();
|
||||
ui->provider->addItem(name, id);
|
||||
}
|
||||
|
||||
QString qhandler_id = captions->handler_id.c_str();
|
||||
int idx = ui->provider->findData(qhandler_id);
|
||||
if (idx != -1)
|
||||
ui->provider->setCurrentIndex(idx);
|
||||
|
||||
ui->enable->blockSignals(true);
|
||||
ui->enable->setChecked(!!captions->handler);
|
||||
ui->enable->blockSignals(false);
|
||||
|
||||
vector<locale_info> locales;
|
||||
get_valid_locale_names(locales);
|
||||
|
||||
bool set_language = false;
|
||||
|
||||
ui->language->blockSignals(true);
|
||||
for (int idx = 0; idx < (int)locales.size(); idx++) {
|
||||
locale_info &locale = locales[idx];
|
||||
|
||||
ui->language->addItem(locale.name->array, (int)locale.id);
|
||||
|
||||
if (locale.id == captions->lang_id) {
|
||||
ui->language->setCurrentIndex(idx);
|
||||
set_language = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!set_language && locales.size())
|
||||
ui->language->setCurrentIndex(0);
|
||||
|
||||
ui->language->blockSignals(false);
|
||||
|
||||
if (!locales.size()) {
|
||||
ui->source->setEnabled(false);
|
||||
ui->enable->setEnabled(false);
|
||||
ui->language->setEnabled(false);
|
||||
|
||||
} else if (!set_language) {
|
||||
bool started = !!captions->handler;
|
||||
if (started)
|
||||
captions->stop();
|
||||
|
||||
captions->lang_id = locales[0].id;
|
||||
|
||||
if (started)
|
||||
captions->start();
|
||||
}
|
||||
}
|
||||
|
||||
void CaptionsDialog::on_source_currentIndexChanged(int)
|
||||
{
|
||||
bool started = !!captions->handler;
|
||||
if (started)
|
||||
captions->stop();
|
||||
|
||||
captions->source_name = ui->source->currentText().toUtf8().constData();
|
||||
captions->source = GetWeakSourceByName(captions->source_name.c_str());
|
||||
|
||||
if (started)
|
||||
captions->start();
|
||||
}
|
||||
|
||||
void CaptionsDialog::on_enable_clicked(bool checked)
|
||||
{
|
||||
if (checked) {
|
||||
captions->start();
|
||||
if (!captions->handler) {
|
||||
ui->enable->blockSignals(true);
|
||||
ui->enable->setChecked(false);
|
||||
ui->enable->blockSignals(false);
|
||||
}
|
||||
} else {
|
||||
captions->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void CaptionsDialog::on_language_currentIndexChanged(int)
|
||||
{
|
||||
bool started = !!captions->handler;
|
||||
if (started)
|
||||
captions->stop();
|
||||
|
||||
captions->lang_id = (LANGID)ui->language->currentData().toInt();
|
||||
|
||||
if (started)
|
||||
captions->start();
|
||||
}
|
||||
|
||||
void CaptionsDialog::on_provider_currentIndexChanged(int idx)
|
||||
{
|
||||
bool started = !!captions->handler;
|
||||
if (started)
|
||||
captions->stop();
|
||||
|
||||
captions->handler_id = ui->provider->itemData(idx).toString().toUtf8().constData();
|
||||
|
||||
if (started)
|
||||
captions->start();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
static void caption_text(const std::string &text)
|
||||
{
|
||||
OBSOutputAutoRelease output = obs_frontend_get_streaming_output();
|
||||
if (output) {
|
||||
obs_output_output_caption_text1(output, text.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static void audio_capture(void *, obs_source_t *, const struct audio_data *audio, bool)
|
||||
{
|
||||
captions->handler->push_audio(audio);
|
||||
}
|
||||
|
||||
void obs_captions::start()
|
||||
{
|
||||
if (!captions->handler && valid_lang(lang_id)) {
|
||||
wchar_t wname[256];
|
||||
|
||||
auto pair = handler_types.find(handler_id);
|
||||
if (pair == handler_types.end()) {
|
||||
warn("Failed to find handler '%s'", handler_id.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!LCIDToLocaleName(lang_id, wname, 256, 0)) {
|
||||
warn("Failed to get locale name: %d", (int)GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
size_t len = (size_t)wcslen(wname);
|
||||
|
||||
string lang_name;
|
||||
lang_name.resize(len);
|
||||
|
||||
for (size_t i = 0; i < len; i++)
|
||||
lang_name[i] = (char)wname[i];
|
||||
|
||||
OBSSource s = OBSGetStrongRef(source);
|
||||
if (!s) {
|
||||
warn("Source invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
captions_handler *h = pair->second.create(caption_text, lang_name);
|
||||
handler.reset(h);
|
||||
|
||||
OBSSource s = OBSGetStrongRef(source);
|
||||
obs_source_add_audio_capture_callback(s, audio_capture, nullptr);
|
||||
|
||||
} catch (std::string text) {
|
||||
QWidget *window = (QWidget *)obs_frontend_get_main_window();
|
||||
|
||||
warn("Failed to create handler: %s", text.c_str());
|
||||
|
||||
QMessageBox::warning(window, obs_module_text("Captions.Error.GenericFail"), text.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void obs_captions::stop()
|
||||
{
|
||||
OBSSource s = OBSGetStrongRef(source);
|
||||
if (s)
|
||||
obs_source_remove_audio_capture_callback(s, audio_capture, nullptr);
|
||||
handler.reset();
|
||||
}
|
||||
|
||||
static bool get_locale_name(LANGID id, char *out)
|
||||
{
|
||||
wchar_t name[256];
|
||||
|
||||
int size = GetLocaleInfoW(id, LOCALE_SENGLISHLANGUAGENAME, name, 256);
|
||||
if (size <= 0)
|
||||
return false;
|
||||
|
||||
os_wcs_to_utf8(name, 0, out, 256);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool valid_lang(LANGID id)
|
||||
{
|
||||
ComPtr<ISpObjectToken> token;
|
||||
wchar_t lang_str[32];
|
||||
HRESULT hr;
|
||||
|
||||
_snwprintf(lang_str, 31, L"language=%x", (int)id);
|
||||
|
||||
hr = SpFindBestToken(SPCAT_RECOGNIZERS, lang_str, nullptr, &token);
|
||||
return SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
static void get_valid_locale_names(vector<locale_info> &locales)
|
||||
{
|
||||
locale_info cur;
|
||||
char locale_name[256];
|
||||
|
||||
static const LANGID default_locales[] = {0x0409, 0x0401, 0x0402, 0x0403, 0x0404, 0x0405, 0x0406, 0x0407, 0x0408,
|
||||
0x040a, 0x040b, 0x040c, 0x040d, 0x040e, 0x040f, 0x0410, 0x0411, 0x0412,
|
||||
0x0413, 0x0414, 0x0415, 0x0416, 0x0417, 0x0418, 0x0419, 0x041a, 0};
|
||||
|
||||
/* ---------------------------------- */
|
||||
|
||||
LANGID def_id = GetUserDefaultUILanguage();
|
||||
LANGID id = def_id;
|
||||
if (valid_lang(id) && get_locale_name(id, locale_name)) {
|
||||
dstr_copy(cur.name, obs_module_text("Captions.CurrentSystemLanguage"));
|
||||
dstr_replace(cur.name, "%1", locale_name);
|
||||
cur.id = id;
|
||||
|
||||
locales.push_back(std::move(cur));
|
||||
}
|
||||
|
||||
/* ---------------------------------- */
|
||||
|
||||
const LANGID *locale = default_locales;
|
||||
|
||||
while (*locale) {
|
||||
id = *locale;
|
||||
|
||||
if (id != def_id && valid_lang(id) && get_locale_name(id, locale_name)) {
|
||||
|
||||
dstr_copy(cur.name, locale_name);
|
||||
cur.id = id;
|
||||
|
||||
locales.push_back(std::move(cur));
|
||||
}
|
||||
|
||||
locale++;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
extern captions_handler_info mssapi_info;
|
||||
|
||||
obs_captions::obs_captions()
|
||||
{
|
||||
register_handler("mssapi", mssapi_info);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
extern "C" void FreeCaptions()
|
||||
{
|
||||
delete captions;
|
||||
captions = nullptr;
|
||||
}
|
||||
|
||||
static void obs_event(enum obs_frontend_event event, void *)
|
||||
{
|
||||
if (event == OBS_FRONTEND_EVENT_EXIT)
|
||||
FreeCaptions();
|
||||
}
|
||||
|
||||
static void save_caption_data(obs_data_t *save_data, bool saving, void *)
|
||||
{
|
||||
if (saving) {
|
||||
OBSDataAutoRelease obj = obs_data_create();
|
||||
|
||||
obs_data_set_string(obj, "source", captions->source_name.c_str());
|
||||
obs_data_set_bool(obj, "enabled", !!captions->handler);
|
||||
obs_data_set_int(obj, "lang_id", captions->lang_id);
|
||||
obs_data_set_string(obj, "provider", captions->handler_id.c_str());
|
||||
|
||||
obs_data_set_obj(save_data, "captions", obj);
|
||||
} else {
|
||||
captions->stop();
|
||||
|
||||
OBSDataAutoRelease obj = obs_data_get_obj(save_data, "captions");
|
||||
if (!obj)
|
||||
obj = obs_data_create();
|
||||
|
||||
obs_data_set_default_int(obj, "lang_id", GetUserDefaultUILanguage());
|
||||
obs_data_set_default_string(obj, "provider", DEFAULT_HANDLER);
|
||||
|
||||
bool enabled = obs_data_get_bool(obj, "enabled");
|
||||
captions->source_name = obs_data_get_string(obj, "source");
|
||||
captions->lang_id = (int)obs_data_get_int(obj, "lang_id");
|
||||
captions->handler_id = obs_data_get_string(obj, "provider");
|
||||
captions->source = GetWeakSourceByName(captions->source_name.c_str());
|
||||
|
||||
if (enabled)
|
||||
captions->start();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void InitCaptions()
|
||||
{
|
||||
QAction *action = (QAction *)obs_frontend_add_tools_menu_qaction(obs_module_text("Captions"));
|
||||
|
||||
captions = new obs_captions;
|
||||
|
||||
auto cb = []() {
|
||||
obs_frontend_push_ui_translation(obs_module_get_string);
|
||||
|
||||
QWidget *window = (QWidget *)obs_frontend_get_main_window();
|
||||
|
||||
CaptionsDialog dialog(window);
|
||||
dialog.exec();
|
||||
|
||||
obs_frontend_pop_ui_translation();
|
||||
};
|
||||
|
||||
obs_frontend_add_save_callback(save_caption_data, nullptr);
|
||||
obs_frontend_add_event_callback(obs_event, nullptr);
|
||||
|
||||
action->connect(action, &QAction::triggered, cb);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <memory>
|
||||
|
||||
#include "ui_captions.h"
|
||||
|
||||
class CaptionsDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
std::unique_ptr<Ui_CaptionsDialog> ui;
|
||||
|
||||
public:
|
||||
CaptionsDialog(QWidget *parent);
|
||||
|
||||
public slots:
|
||||
void on_source_currentIndexChanged(int idx);
|
||||
void on_enable_clicked(bool checked);
|
||||
void on_language_currentIndexChanged(int idx);
|
||||
void on_provider_currentIndexChanged(int idx);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
1 VERSIONINFO
|
||||
FILEVERSION ${OBS_VERSION_MAJOR},${OBS_VERSION_MINOR},${OBS_VERSION_PATCH},0
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904B0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "${OBS_COMPANY_NAME}"
|
||||
VALUE "FileDescription", "OBS Frontend Tools"
|
||||
VALUE "FileVersion", "${OBS_VERSION_CANONICAL}"
|
||||
VALUE "ProductName", "${OBS_PRODUCT_NAME}"
|
||||
VALUE "ProductVersion", "${OBS_VERSION_CANONICAL}"
|
||||
VALUE "Comments", "${OBS_COMMENTS}"
|
||||
VALUE "LegalCopyright", "${OBS_LEGAL_COPYRIGHT}"
|
||||
VALUE "InternalName", "frontend-tools"
|
||||
VALUE "OriginalFilename", "frontend-tools"
|
||||
END
|
||||
END
|
||||
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x0409, 0x04B0
|
||||
END
|
||||
END
|
||||
@@ -0,0 +1,37 @@
|
||||
SceneSwitcher="Outomatiese toneelwisselaar"
|
||||
SceneSwitcher.ActiveOrNotActive="Toneelwisselaar is:"
|
||||
InvalidRegex.Title="Ongeldige reguliere uitdrukking"
|
||||
InvalidRegex.Text="Die ingevoerde reguliere uitdrukking is ongeldig."
|
||||
Active="Aktief"
|
||||
Inactive="Onaktief"
|
||||
Start="Begin"
|
||||
Captions="Ondertiteling (eksperimenteel)"
|
||||
Captions.AudioSource="Oudiobron"
|
||||
Captions.CurrentSystemLanguage="Huidige stelseltaal (%1)"
|
||||
Captions.Provider="Voorsiener"
|
||||
Captions.Error.GenericFail="Kon nie ondertiteling begin nie"
|
||||
OutputTimer.Stream="Stop stroom na:"
|
||||
OutputTimer.Record="Stop opname na:"
|
||||
OutputTimer.Stream.StoppingIn="Stroom stop oor:"
|
||||
OutputTimer.Record.StoppingIn="Opname stop oor:"
|
||||
Scripts="Skripte"
|
||||
LoadedScripts="Gelaaide skripte"
|
||||
AddScripts="Voeg skripte toe"
|
||||
RemoveScripts="Verwyder skripte"
|
||||
ReloadScripts="Herlaai skripte"
|
||||
EditScript="Wysig skrip"
|
||||
Reload="Gerlaai"
|
||||
OpenFileLocation="Open lêerligging"
|
||||
PythonSettings="Python-instellings"
|
||||
PythonSettings.PythonInstallPath32bit="Python Installasiepad (32 bis)"
|
||||
PythonSettings.PythonInstallPath64bit="Python Installasiepad (64 bis)"
|
||||
PythonSettings.BrowsePythonPath="Blaai na Python pad"
|
||||
PythonSettings.PythonVersion="Gelaaide Python-weergawe: %1"
|
||||
PythonSettings.PythonNotLoaded="Python nie tans gelaai nie"
|
||||
PythonSettings.AlreadyLoaded.Title="Python is reeds gelaai"
|
||||
PythonSettings.AlreadyLoaded.Message="’n Kopie van Python %1 is reeds gelaai. Om die nuutgekose Python-weergawe te laai moet u OBS herbegin."
|
||||
ScriptLogWindow="Skriplogboek"
|
||||
Description="Beskrywing"
|
||||
ScriptDescriptionLink.Text="Open hierdie skakel in u verstekwebblaaier?"
|
||||
FileFilter.ScriptFiles="Skriplêers"
|
||||
FileFilter.AllFiles="Alle lêers"
|
||||
@@ -0,0 +1,42 @@
|
||||
SceneSwitcher="Cambiador automatico de scenas"
|
||||
SceneSwitcher.OnNoMatch="Cuan no coincide garra finestra:"
|
||||
SceneSwitcher.OnNoMatch.DontSwitch="No cambiar"
|
||||
SceneSwitcher.OnNoMatch.SwitchTo="Cambiar ta:"
|
||||
SceneSwitcher.CheckInterval="Comprebar lo titol d'a finestra activa cada:"
|
||||
SceneSwitcher.ActiveOrNotActive="Lo cambiador de scena ye:"
|
||||
InvalidRegex.Title="Expresión regular no valida"
|
||||
InvalidRegex.Text="La expresión regular que has escrito no ye valida."
|
||||
Active="Activo"
|
||||
Inactive="Inactivo"
|
||||
Start="Iniciar"
|
||||
Stop="Aturar"
|
||||
Captions="Subtitols (experimental)"
|
||||
Captions.AudioSource="Fuent d'audio"
|
||||
Captions.CurrentSystemLanguage="Idioma d'o sistema actual (%1)"
|
||||
Captions.Provider="Proveyedor"
|
||||
Captions.Error.GenericFail="No s'han puesto iniciar los subtitols"
|
||||
OutputTimer="Temporizador de salida"
|
||||
OutputTimer.Stream="Aturar la transmisión dimpués de:"
|
||||
OutputTimer.Record="Aturar la gravación dimpués de:"
|
||||
OutputTimer.Stream.StoppingIn="La transmisión s'aturará d'aquí a:"
|
||||
OutputTimer.Record.StoppingIn="La gravación s'aturará d'aquí a:"
|
||||
OutputTimer.Stream.EnableEverytime="Activar lo temporizador de transmisión cada vegada"
|
||||
OutputTimer.Record.EnableEverytime="Activar lo temporizador de gravación cada vegada"
|
||||
OutputTimer.Record.PauseTimer="Pausar lo temporizador en pausar la gravación"
|
||||
LoadedScripts="Scripts cargaus"
|
||||
AddScripts="Anyadir scripts"
|
||||
RemoveScripts="Elminar scripts"
|
||||
ReloadScripts="Recargar scripts"
|
||||
EditScript="Editar lo script"
|
||||
Reload="Recargar"
|
||||
OpenFileLocation="Ubrir la localización d'o fichero"
|
||||
PythonSettings="Achustes de Python"
|
||||
PythonSettings.PythonInstallPath32bit="Camín d'instalación de Python (32 bits)"
|
||||
PythonSettings.PythonInstallPath64bit="Camín d'instalación de Python (64 bits)"
|
||||
PythonSettings.BrowsePythonPath="Mirar lo camín de Python"
|
||||
ScriptLogWindow="Rechistro de scripts"
|
||||
Description="Descripción"
|
||||
ScriptDescriptionLink.Text="Ubrir este vinclo en o tuyo navegador per defecto?"
|
||||
ScriptDescriptionLink.OpenURL="Ubrir la URL"
|
||||
FileFilter.ScriptFiles="Fichers script"
|
||||
FileFilter.AllFiles="Toz los Fichers"
|
||||
@@ -0,0 +1,48 @@
|
||||
SceneSwitcher="مبدل المَشاهِد التلقائي"
|
||||
SceneSwitcher.OnNoMatch="عندما لا تتطابق أي نافذة:"
|
||||
SceneSwitcher.OnNoMatch.DontSwitch="لا تقم بالتبديل"
|
||||
SceneSwitcher.OnNoMatch.SwitchTo="التبديل إلى:"
|
||||
SceneSwitcher.CheckInterval="التأكد من عنوان النافذة النشطة كل:"
|
||||
SceneSwitcher.ActiveOrNotActive="حالة مبدل المشاهد:"
|
||||
InvalidRegex.Title="تعبير نمطي غير صحيح"
|
||||
InvalidRegex.Text="التعبير النمطي الذي أدخلته غير صحيح."
|
||||
Active="مفعّل"
|
||||
Inactive="غير مُفعّل"
|
||||
Start="ابدأ"
|
||||
Stop="ايقاف"
|
||||
Captions="ترجمة فورية (تجريبي)"
|
||||
Captions.AudioSource="مصدر الصوت"
|
||||
Captions.CurrentSystemLanguage="لغة النظام الحالية (%1)"
|
||||
Captions.Provider="موفر الخدمة"
|
||||
Captions.Error.GenericFail="فشل في تشغيل الترجمة الفورية"
|
||||
OutputTimer="مؤقِّت الإخراج"
|
||||
OutputTimer.Stream="إيقاف البث بعد:"
|
||||
OutputTimer.Record="إيقاف التسجيل بعد:"
|
||||
OutputTimer.Stream.StoppingIn="سيتم إيقاف البث بعد:"
|
||||
OutputTimer.Record.StoppingIn="سيتم إيقاف التسجيل بعد:"
|
||||
OutputTimer.Stream.EnableEverytime="تفعيل مؤقِّت البث في كل مرة"
|
||||
OutputTimer.Record.EnableEverytime="تفعيل مؤقِّت التسجيل في كل مرة"
|
||||
OutputTimer.Record.PauseTimer="إيقاف المُؤَقِت عندما يكون التسجيل متوقف"
|
||||
Scripts="سكريبتات"
|
||||
LoadedScripts="السكريبتات المحملة"
|
||||
AddScripts="إضافة سكريبت"
|
||||
RemoveScripts="إزالة السكريبت"
|
||||
ReloadScripts="إعادة تحميل السكربتات"
|
||||
EditScript="تحرير سكريبت"
|
||||
Reload="إعادة تحميل"
|
||||
OpenFileLocation="فتح موقع الملف"
|
||||
PythonSettings="إعدادات بايثون"
|
||||
PythonSettings.PythonInstallPath32bit="مسار تثبيت بايثون (32 بت)"
|
||||
PythonSettings.PythonInstallPath64bit="مسار تثبيت بايثون (64 بت)"
|
||||
PythonSettings.BrowsePythonPath="استعراض مسار البايثون"
|
||||
PythonSettings.PythonVersion="إصدار البايثون المُحمل: %1"
|
||||
PythonSettings.PythonNotLoaded="لم يتم تحميل بايثون حاليًا"
|
||||
PythonSettings.AlreadyLoaded.Title="تم تحميل بايثون بالفعل"
|
||||
PythonSettings.AlreadyLoaded.Message="نسخة بايثون %1 قد تم تحميلها. لتحميل إصدار بايثون الذي تم اختياره للتو، الرجاء إعادة تشغيل OBS."
|
||||
ScriptLogWindow="سجل السكريبت"
|
||||
Description="الوصف"
|
||||
ScriptDescriptionLink.Text="فتح هذا الرابط في متصفحك الافتراضي ؟"
|
||||
ScriptDescriptionLink.Text.Url="الرابط: %1"
|
||||
ScriptDescriptionLink.OpenURL="فتح الرابط"
|
||||
FileFilter.ScriptFiles="ملفات سكريبت"
|
||||
FileFilter.AllFiles="جميع الملفات"
|
||||
@@ -0,0 +1,47 @@
|
||||
SceneSwitcher="Avtomatik Səhnə Dəyişdirici"
|
||||
SceneSwitcher.OnNoMatch="Heç bir pəncərəyə uyğun gəlməyəndə:"
|
||||
SceneSwitcher.OnNoMatch.DontSwitch="Dəyişdirmə"
|
||||
SceneSwitcher.OnNoMatch.SwitchTo="Dəyişdir:"
|
||||
SceneSwitcher.CheckInterval="Aktiv pəncərə başlığını yoxla:"
|
||||
SceneSwitcher.ActiveOrNotActive="Səhnə Dəyişdirici:"
|
||||
InvalidRegex.Title="Etibarsız Müntəzəm İfadə"
|
||||
InvalidRegex.Text="Daxil etdiyiniz müntəzəm ifadə etibarsızdır."
|
||||
Active="Aktiv"
|
||||
Inactive="Qeyri aktiv"
|
||||
Start="Başlat"
|
||||
Stop="Dayandır"
|
||||
Captions="Altyazılar (Təcrübi)"
|
||||
Captions.AudioSource="Səs mənbəsi"
|
||||
Captions.CurrentSystemLanguage="Hazırki Sistem Dili (%1)"
|
||||
Captions.Provider="Təchizatçı"
|
||||
Captions.Error.GenericFail="Altyazı başladılmadı"
|
||||
OutputTimer="Çıxış Vaxtölçəni"
|
||||
OutputTimer.Stream="Yayımı dayandır:"
|
||||
OutputTimer.Record="Yazmanı dayandır:"
|
||||
OutputTimer.Stream.StoppingIn="Yayım dayandırılır:"
|
||||
OutputTimer.Record.StoppingIn="Yazma dayandırılır:"
|
||||
OutputTimer.Stream.EnableEverytime="Hər dəfə yayım vaxtölçənini fəallaşdır"
|
||||
OutputTimer.Record.EnableEverytime="Hər dəfə yazma vaxtölçənini fəallaşdır"
|
||||
OutputTimer.Record.PauseTimer="Yazmaya fasilə veriləndə vaxtölçənə də fasilə ver"
|
||||
Scripts="Skriptlər"
|
||||
LoadedScripts="Yüklənmiş skriptlər"
|
||||
AddScripts="Skript əlavə et"
|
||||
RemoveScripts="Skripti çıxart"
|
||||
ReloadScripts="Skripti yenidən yüklə"
|
||||
EditScript="Kodu redaktə et"
|
||||
Reload="Yenidən yüklə"
|
||||
OpenFileLocation="Fayl yerləşməsini aç"
|
||||
PythonSettings="Python Tənzimləmələri"
|
||||
PythonSettings.PythonInstallPath32bit="Python Quraşdırma Yolu (32 bit)"
|
||||
PythonSettings.PythonInstallPath64bit="Python Quraşdırma Yolu (64 bit)"
|
||||
PythonSettings.BrowsePythonPath="Python yoluna nəzər yetir"
|
||||
PythonSettings.PythonVersion="Yüklənmiş Python Versiyası:%1"
|
||||
PythonSettings.PythonNotLoaded="Python hazırda yüklənməyib"
|
||||
PythonSettings.AlreadyLoaded.Title="Python artıq yüklənib"
|
||||
PythonSettings.AlreadyLoaded.Message="Python %1 üçün bir kopya artıq yüklüdür. Seçilmiş yeni Python versiyasını yükləmək üçün OBS'i yenidən başlat."
|
||||
ScriptLogWindow="Sktript jurnalı"
|
||||
Description="Açıqlama"
|
||||
ScriptDescriptionLink.Text="Bu bağlantı ilkin veb səyyahında açılsın?"
|
||||
ScriptDescriptionLink.OpenURL="URL-ni aç"
|
||||
FileFilter.ScriptFiles="Skript faylları"
|
||||
FileFilter.AllFiles="Bütün fayllar"
|
||||
@@ -0,0 +1,8 @@
|
||||
Active="Әүҙемле"
|
||||
Inactive="Әүҙемһеҙ"
|
||||
Start="Башларға"
|
||||
Stop="Туҡтатырға"
|
||||
Captions.AudioSource="Тауыш сығанағы"
|
||||
Captions.CurrentSystemLanguage="Системаның ағымдағы теле (%1)"
|
||||
Scripts="Скрипттар"
|
||||
FileFilter.AllFiles="Барлыҡ файлдар"
|
||||
@@ -0,0 +1,47 @@
|
||||
SceneSwitcher="Аўтаматычны пераключальнік сцэн"
|
||||
SceneSwitcher.OnNoMatch="Калі няма адпаведных вокнаў:"
|
||||
SceneSwitcher.OnNoMatch.DontSwitch="Не пераключацца"
|
||||
SceneSwitcher.OnNoMatch.SwitchTo="Пераключыцца на"
|
||||
SceneSwitcher.CheckInterval="Правяраць загаловак актыўнага акна раз на"
|
||||
SceneSwitcher.ActiveOrNotActive="Стан пераключальніка:"
|
||||
InvalidRegex.Title="Няправільны рэгулярны выраз"
|
||||
InvalidRegex.Text="Уведзены рэгулярны выраз ёсць памылковым."
|
||||
Active="Актыўны"
|
||||
Inactive="Неактыўны"
|
||||
Start="Пачаць"
|
||||
Stop="Спыніць"
|
||||
Captions="Субцітры (эксперыментальны)"
|
||||
Captions.AudioSource="Крыніца гуку"
|
||||
Captions.CurrentSystemLanguage="Бягучая мова сістэмы (%1)"
|
||||
Captions.Provider="Пастаўшчык"
|
||||
Captions.Error.GenericFail="Не атрымалася запусціць субцітры"
|
||||
OutputTimer="Таймер вываду"
|
||||
OutputTimer.Stream="Спыніць стрым праз"
|
||||
OutputTimer.Record="Спыніць запіс праз"
|
||||
OutputTimer.Stream.StoppingIn="Стрым спыніцца праз"
|
||||
OutputTimer.Record.StoppingIn="Запіс спыніцца праз"
|
||||
OutputTimer.Stream.EnableEverytime="Заўжды ўключаць таймер стрыма"
|
||||
OutputTimer.Record.EnableEverytime="Заўжды ўключаць таймер запісу"
|
||||
OutputTimer.Record.PauseTimer="Прыпыняць таймер пры прыпыненні запісу"
|
||||
Scripts="Скрыпты"
|
||||
LoadedScripts="Загружаныя скрыпты"
|
||||
AddScripts="Дадаць скрыпты"
|
||||
RemoveScripts="Выдаліць скрыпты"
|
||||
ReloadScripts="Перазагрузіць скрыпты"
|
||||
EditScript="Рэдагаваць"
|
||||
Reload="Перазагрузіць"
|
||||
OpenFileLocation="Адкрыць папку з файлам"
|
||||
PythonSettings="Налады Python"
|
||||
PythonSettings.PythonInstallPath32bit="Шлях да Python (32 біт)"
|
||||
PythonSettings.PythonInstallPath64bit="Шлях да Python (64 біт)"
|
||||
PythonSettings.BrowsePythonPath="Знайсці шлях да Python"
|
||||
PythonSettings.PythonVersion="Загружаная версія Python: %1"
|
||||
PythonSettings.PythonNotLoaded="Python не загружаны"
|
||||
PythonSettings.AlreadyLoaded.Title="Python ужо загружаны"
|
||||
PythonSettings.AlreadyLoaded.Message="Python %1 ужо загружаны. Каб загрузіць версію, выбраную зараз, перазапусціце OBS."
|
||||
ScriptLogWindow="Журнал"
|
||||
Description="Апісанне"
|
||||
ScriptDescriptionLink.Text="Адкрыць спасылку ў прадвызначаным вэб-браўзеры?"
|
||||
ScriptDescriptionLink.OpenURL="Адкрыць URL"
|
||||
FileFilter.ScriptFiles="Файлы скрыптаў"
|
||||
FileFilter.AllFiles="Усе файлы"
|
||||
@@ -0,0 +1,46 @@
|
||||
SceneSwitcher="Автоматичен превключвател на сцени"
|
||||
SceneSwitcher.OnNoMatch="Когато прозорците не съвпадат:"
|
||||
SceneSwitcher.OnNoMatch.DontSwitch="Без превключване"
|
||||
SceneSwitcher.OnNoMatch.SwitchTo="Превключи на:"
|
||||
SceneSwitcher.CheckInterval="Проверка название на активния прозорец всеки:"
|
||||
SceneSwitcher.ActiveOrNotActive="Превключвателят на сцени е:"
|
||||
InvalidRegex.Title="Невалиден регулярен израз"
|
||||
InvalidRegex.Text="Въведохте невалиден регулярен израз."
|
||||
Active="Активен"
|
||||
Inactive="Изключен"
|
||||
Start="Стартирай"
|
||||
Stop="Спри"
|
||||
Captions="Надписи (експериментално)"
|
||||
Captions.AudioSource="Звуков източник"
|
||||
Captions.CurrentSystemLanguage="Текущ език на системата (%1)"
|
||||
Captions.Provider="Доставчик"
|
||||
Captions.Error.GenericFail="Неуспешно зареждане на надписи"
|
||||
OutputTimer="Изходен таймер"
|
||||
OutputTimer.Stream="Прекъсни излъчването след:"
|
||||
OutputTimer.Record="Прекъсни записването след:"
|
||||
OutputTimer.Stream.StoppingIn="Излъчването ще спре след:"
|
||||
OutputTimer.Record.StoppingIn="Записването ще спре след:"
|
||||
OutputTimer.Stream.EnableEverytime="Показвай брояча на излъчването всеки път"
|
||||
OutputTimer.Record.EnableEverytime="Включване на таймера при запис всеки път"
|
||||
OutputTimer.Record.PauseTimer="Пауза на таймера при пауза на записа"
|
||||
Scripts="Скриптове"
|
||||
LoadedScripts="Заредени скриптове"
|
||||
AddScripts="Добавяне на скриптове"
|
||||
RemoveScripts="Премахване на скриптове"
|
||||
ReloadScripts="Презареждане на скриптове"
|
||||
EditScript="Редактиране на кода"
|
||||
Reload="Презареди"
|
||||
OpenFileLocation="Отворете местоположението на файла"
|
||||
PythonSettings="Настройки на Python"
|
||||
PythonSettings.PythonInstallPath32bit="Път за инсталация на Python (32 бита)"
|
||||
PythonSettings.PythonInstallPath64bit="Път за инсталация на Python (64 бита)"
|
||||
PythonSettings.BrowsePythonPath="Разглеждане на пътя до Python"
|
||||
PythonSettings.PythonVersion="Заредена Python Версия: %1"
|
||||
PythonSettings.PythonNotLoaded="Python не можа да бъде зареден"
|
||||
PythonSettings.AlreadyLoaded.Title="Python вече е зареден"
|
||||
PythonSettings.AlreadyLoaded.Message="Копие на Python %1 вече е заредено. За да заредите друга версия на Python, моля рестартирайте OBS."
|
||||
ScriptLogWindow="Записи на скриптове"
|
||||
Description="Описание"
|
||||
ScriptDescriptionLink.Text="Да се отвори ли тази връзка в уеб браузъра ви по подразбиране?"
|
||||
FileFilter.ScriptFiles="Файлове със скрипт"
|
||||
FileFilter.AllFiles="Всички файлове"
|
||||
@@ -0,0 +1,44 @@
|
||||
SceneSwitcher="স্বয়ংক্রিয় দৃশ্য পরিবর্তক"
|
||||
SceneSwitcher.OnNoMatch="যখন কোনও উইন্ডো মেলে নাঃ"
|
||||
SceneSwitcher.OnNoMatch.DontSwitch="স্যুইচ করবেন না"
|
||||
SceneSwitcher.OnNoMatch.SwitchTo="পরিবর্তন করুনঃ"
|
||||
SceneSwitcher.CheckInterval="সক্রিয় উইন্ডো শিরোনাম চেক করুন প্রতিঃ"
|
||||
SceneSwitcher.ActiveOrNotActive="দৃশ্য পরিবর্তনকারী হলঃ"
|
||||
InvalidRegex.Title="অবৈধ নিয়মিত এক্সপ্রেশন"
|
||||
InvalidRegex.Text="যে রেগুলার এক্সপ্রেশন প্রবেশ করেছ তা ভুল।"
|
||||
Active="সক্রিয়"
|
||||
Inactive="নিষ্ক্রিয়"
|
||||
Start="শুরু"
|
||||
Stop="থামুন"
|
||||
Captions="ক্যাপশন (পরীক্ষামূলক)"
|
||||
Captions.AudioSource="অডিও উৎস"
|
||||
Captions.CurrentSystemLanguage="বর্তমান সিস্টেমের ভাষা (%1)"
|
||||
Captions.Provider="প্রোভাইডার"
|
||||
Captions.Error.GenericFail="ক্যাপশন শুরু করতে ব্যর্থ"
|
||||
OutputTimer="আউটপুট টাইমার"
|
||||
OutputTimer.Stream="এর পরে বন্ধ।:"
|
||||
OutputTimer.Record="এর পরে রেকর্ডিং বন্ধ করুন:"
|
||||
OutputTimer.Stream.StoppingIn="স্ট্রিমিং থামবে:"
|
||||
OutputTimer.Record.StoppingIn="রেকর্ডিং থামবেঃ"
|
||||
OutputTimer.Stream.EnableEverytime="প্রতিবার স্ট্রিমিং টাইমার চালু করুন"
|
||||
OutputTimer.Record.EnableEverytime="প্রতিবার রেকর্ডিং টাইমার চালু করুন"
|
||||
OutputTimer.Record.PauseTimer="রেকর্ডিং থামান যখন টাইমার থামে"
|
||||
Scripts="স্ক্রিপ্টসমূহ"
|
||||
LoadedScripts="স্ক্রিপ্টসমূহ লোড করা হয়েছে"
|
||||
AddScripts="স্ক্রিপ্ট যুক্ত করুন"
|
||||
RemoveScripts="স্ক্রিপ্ট সরান"
|
||||
ReloadScripts="স্ক্রিপ্টসমূহ পুনঃলোড করুন"
|
||||
EditScript="স্ক্রিপ্ট সম্পাদনা"
|
||||
Reload="পুনঃলোড"
|
||||
OpenFileLocation="ফাইল অবস্থান খুলুন"
|
||||
PythonSettings="পাইথন সেটিংসসমূহ"
|
||||
PythonSettings.PythonInstallPath32bit="পাইথন ইনস্টল পাথ (৩২ বিট)"
|
||||
PythonSettings.PythonInstallPath64bit="পাইথন ইনস্টল পাথ (৬৪ বিট)"
|
||||
PythonSettings.BrowsePythonPath="পাইথন পাথ ব্রাউজ করুন"
|
||||
ScriptLogWindow="স্ক্রিপ্ট লগ"
|
||||
Description="বর্ণনা"
|
||||
ScriptDescriptionLink.Text="আপনার ডিফল্ট ওয়েব ব্রাউজারে এই লিঙ্কটি খুলবেন?"
|
||||
ScriptDescriptionLink.Text.Url="ইউআরএল: %1"
|
||||
ScriptDescriptionLink.OpenURL="ইউআরএল খুলুন"
|
||||
FileFilter.ScriptFiles="স্ক্রিপ্ট ফাইল"
|
||||
FileFilter.AllFiles="সকল ফাইল"
|
||||
@@ -0,0 +1,46 @@
|
||||
SceneSwitcher="Canviador d'escena automàtica"
|
||||
SceneSwitcher.OnNoMatch="Quan no coincideixi amb cap finestra:"
|
||||
SceneSwitcher.OnNoMatch.DontSwitch="No canviar"
|
||||
SceneSwitcher.OnNoMatch.SwitchTo="Canvia a:"
|
||||
SceneSwitcher.CheckInterval="Comprova el títol de la finestra activa cada:"
|
||||
SceneSwitcher.ActiveOrNotActive="El canviador de escena està:"
|
||||
InvalidRegex.Title="Expressió regular no vàlida"
|
||||
InvalidRegex.Text="La expressió regular que heu introduït no es vàlida."
|
||||
Active="Actiu"
|
||||
Inactive="Inactiu"
|
||||
Start="Inicia"
|
||||
Stop="Atura"
|
||||
Captions="Subtítols (Experimental)"
|
||||
Captions.AudioSource="Font d'àudio"
|
||||
Captions.CurrentSystemLanguage="Idioma actual del sistema (%1)"
|
||||
Captions.Provider="Proveïdor"
|
||||
Captions.Error.GenericFail="No s'ha pogut iniciar els subtítols"
|
||||
OutputTimer="Temporitzador de sortida"
|
||||
OutputTimer.Stream="Atura la transmissió després de:"
|
||||
OutputTimer.Record="Atura la gravació després de:"
|
||||
OutputTimer.Stream.StoppingIn="La transmissió s'aturarà en:"
|
||||
OutputTimer.Record.StoppingIn="La gravació s'aturarà en:"
|
||||
OutputTimer.Stream.EnableEverytime="Activa el temporitzador en cada transmissió"
|
||||
OutputTimer.Record.EnableEverytime="Activa el temporitzador en cada enregistrament"
|
||||
OutputTimer.Record.PauseTimer="Pausa el temporitzador en pausar l'enregistrament"
|
||||
LoadedScripts="Scripts carregats"
|
||||
AddScripts="Afegeix scripts"
|
||||
RemoveScripts="Suprimeix scripts"
|
||||
ReloadScripts="Torna a carregar els scripts"
|
||||
EditScript="Edita l'script"
|
||||
Reload="Recarrega"
|
||||
OpenFileLocation="Obre la ubicació del fitxer"
|
||||
PythonSettings="Configuració del Python"
|
||||
PythonSettings.PythonInstallPath32bit="Camí d'instal·lació del Python (32bit)"
|
||||
PythonSettings.PythonInstallPath64bit="Camí d'instal·lació del Python (64bit)"
|
||||
PythonSettings.BrowsePythonPath="Camí del Python"
|
||||
PythonSettings.PythonVersion="Versió de Python carregada: %1"
|
||||
PythonSettings.PythonNotLoaded="El Python no està carregat actualment"
|
||||
PythonSettings.AlreadyLoaded.Title="El Python ja està carregat"
|
||||
PythonSettings.AlreadyLoaded.Message="Ja s'ha carregat una còpia del Python %1. Per carregar la versió del Python seleccionada recentment, reinicieu l 'OBS."
|
||||
ScriptLogWindow="Informe script"
|
||||
Description="Descripció"
|
||||
ScriptDescriptionLink.Text="Voleu obrir aquest enllaç al navegador per defecte?"
|
||||
ScriptDescriptionLink.OpenURL="Obre l'URL"
|
||||
FileFilter.ScriptFiles="Fitxers script"
|
||||
FileFilter.AllFiles="Tots els fitxers"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user