obs-studio/UI/importers/xsplit.cpp
VodBox 19ced32c58 UI: Translate to current OS for all colection imports
Importers are written to convert third-party collection formats to a
Windows OBS scene collection. The Studio importer is capable of
translating scene collections to the correct types for the current
operating system. This change makes it so all imports will be ran
through the Studio translation, not just Studio and SL collections.
2021-10-16 15:34:53 -07:00

545 lines
13 KiB
C++

/******************************************************************************
Copyright (C) 2019-2020 by Dillon Pentz <dillon@vodbox.io>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "importers.hpp"
#include <ctype.h>
#include <QDomDocument>
using namespace std;
using namespace json11;
static int hex_string_to_int(string str)
{
int res = 0;
if (str[0] == '#')
str = str.substr(1);
for (size_t i = 0, l = str.size(); i < l; i++) {
res *= 16;
if (str[0] >= '0' && str[0] <= '9')
res += str[0] - '0';
else
res += str[0] - 'A' + 10;
str = str.substr(1);
}
return res;
}
static Json::object parse_text(QString &config)
{
int start = config.indexOf("*{");
config = config.mid(start + 1);
config.replace("\\", "/");
string err;
Json data = Json::parse(config.toStdString(), err);
if (err != "")
return Json::object{};
string outline = data["outline"].string_value();
int out = 0;
if (outline == "thick")
out = 20;
else if (outline == "thicker")
out = 40;
else if (outline == "thinner")
out = 5;
else if (outline == "thin")
out = 10;
string valign = data["vertAlign"].string_value();
if (valign == "middle")
valign = "center";
Json font = Json::object{{"face", data["fontStyle"]}, {"size", 200}};
return Json::object{
{"text", data["text"]},
{"font", font},
{"outline", out > 0},
{"outline_size", out},
{"outline_color",
hex_string_to_int(data["outlineColor"].string_value())},
{"color", hex_string_to_int(data["color"].string_value())},
{"align", data["textAlign"]},
{"valign", valign},
{"alpha", data["opacity"]}};
}
static Json::array parse_playlist(QString &playlist)
{
Json::array out = Json::array{};
while (true) {
int end = playlist.indexOf('*');
QString path = playlist.left(end);
out.push_back(Json::object{{"value", path.toStdString()}});
int next = playlist.indexOf('|');
if (next == -1)
break;
playlist = playlist.mid(next + 1);
}
return out;
}
static void parse_media_types(QDomNamedNodeMap &attr, Json::object &source,
Json::object &settings)
{
QString playlist = attr.namedItem("FilePlaylist").nodeValue();
if (playlist != "") {
source["id"] = "vlc_source";
settings["playlist"] = parse_playlist(playlist);
QString end_op = attr.namedItem("OpWhenFinished").nodeValue();
if (end_op == "2")
settings["loop"] = true;
} else {
QString url = attr.namedItem("item").nodeValue();
int sep = url.indexOf("://");
if (sep != -1) {
QString prot = url.left(sep);
if (prot == "smlndi") {
source["id"] = "ndi_source";
} else {
source["id"] = "ffmpeg_source";
int info = url.indexOf("\\");
QString input;
if (info != -1) {
input = url.left(info);
} else {
input = url;
}
settings["input"] = input.toStdString();
settings["is_local_file"] = false;
}
} else {
source["id"] = "ffmpeg_source";
settings["local_file"] =
url.replace("\\", "/").toStdString();
settings["is_local_file"] = true;
}
}
}
static Json::object parse_slideshow(QString &config)
{
int start = config.indexOf("images\":[");
if (start == -1)
return Json::object{};
config = config.mid(start + 8);
config.replace("\\\\", "/");
int end = config.indexOf(']');
if (end == -1)
return Json::object{};
string arr = config.left(end + 1).toStdString();
string err;
Json::array files = Json::parse(arr, err).array_items();
if (err != "")
return Json::object{};
Json::array files_out = Json::array{};
for (size_t i = 0; i < files.size(); i++) {
string file = files[i].string_value();
files_out.push_back(Json::object{{"value", file}});
}
QString options = config.mid(end + 1);
options[0] = '{';
Json opt = Json::parse(options.toStdString(), err);
if (err != "")
return Json::object{};
return Json::object{{"randomize", opt["random"]},
{"slide_time",
opt["delay"].number_value() * 1000 + 700},
{"files", files_out}};
}
static bool source_name_exists(const string &name, const Json::array &sources)
{
for (size_t i = 0; i < sources.size(); i++) {
if (sources.at(i)["name"].string_value() == name)
return true;
}
return false;
}
static Json get_source_with_id(const string &src_id, const Json::array &sources)
{
for (size_t i = 0; i < sources.size(); i++) {
if (sources.at(i)["src_id"].string_value() == src_id)
return sources.at(i);
}
return nullptr;
}
static void parse_items(QDomNode &item, Json::array &items,
Json::array &sources)
{
while (!item.isNull()) {
QDomNamedNodeMap attr = item.attributes();
QString srcid = attr.namedItem("srcid").nodeValue();
double vol = attr.namedItem("volume").nodeValue().toDouble();
int type = attr.namedItem("type").nodeValue().toInt();
string name;
Json::object settings;
Json::object source;
string temp_name;
int x = 0;
Json exists = get_source_with_id(srcid.toStdString(), sources);
if (!exists.is_null()) {
name = exists["name"].string_value();
goto skip;
}
name = attr.namedItem("cname").nodeValue().toStdString();
if (name.empty() || name[0] == '\0')
name = attr.namedItem("name").nodeValue().toStdString();
temp_name = name;
while (source_name_exists(temp_name, sources)) {
string new_name = name + " " + to_string(x++);
temp_name = new_name;
}
name = temp_name;
settings = Json::object{};
source = Json::object{{"name", name},
{"src_id", srcid.toStdString()},
{"volume", vol}};
/** type=1 means Media of some kind (Video Playlist, RTSP,
RTMP, NDI or Media File).
type=2 means either a DShow or WASAPI source.
type=4 means an Image source.
type=5 means either a Display or Window Capture.
type=7 means a Game Capture.
type=8 means rendered with a browser, which includes:
Web Page, Image Slideshow, Text.
type=11 means another Scene. **/
if (type == 1) {
parse_media_types(attr, source, settings);
} else if (type == 2) {
QString audio = attr.namedItem("itemaudio").nodeValue();
if (audio.isEmpty()) {
source["id"] = "dshow_input";
} else {
source["id"] = "wasapi_input_capture";
int dev = audio.indexOf("\\wave:") + 6;
QString res =
"{0.0.1.00000000}." + audio.mid(dev);
res = res.toLower();
settings["device_id"] = res.toStdString();
}
} else if (type == 4) {
source["id"] = "image_source";
QString path = attr.namedItem("item").nodeValue();
path.replace("\\", "/");
settings["file"] = path.toStdString();
} else if (type == 5) {
QString opt = attr.namedItem("item").nodeValue();
QDomDocument options;
options.setContent(opt);
QDomNode el = options.documentElement();
QDomNamedNodeMap o_attr = el.attributes();
QString display =
o_attr.namedItem("desktop").nodeValue();
if (!display.isEmpty()) {
source["id"] = "monitor_capture";
int cursor = attr.namedItem("ScrCapShowMouse")
.nodeValue()
.toInt();
settings["capture_cursor"] = cursor == 1;
} else {
source["id"] = "window_capture";
QString exec =
o_attr.namedItem("module").nodeValue();
QString window =
o_attr.namedItem("window").nodeValue();
QString _class =
o_attr.namedItem("class").nodeValue();
int pos = exec.lastIndexOf('\\');
if (_class.isEmpty()) {
_class = "class";
}
QString res = window + ":" + _class + ":" +
exec.mid(pos + 1);
settings["window"] = res.toStdString();
settings["priority"] = 2;
}
} else if (type == 7) {
QString opt = attr.namedItem("item").nodeValue();
opt.replace("&lt;", "<");
opt.replace("&gt;", ">");
opt.replace("&quot;", "\"");
QDomDocument doc;
doc.setContent(opt);
QDomNode el = doc.documentElement();
QDomNamedNodeMap o_attr = el.attributes();
QString name = o_attr.namedItem("wndname").nodeValue();
QString exec =
o_attr.namedItem("imagename").nodeValue();
QString res = name = "::" + exec;
source["id"] = "game_capture";
settings["window"] = res.toStdString();
settings["capture_mode"] = "window";
} else if (type == 8) {
QString plugin = attr.namedItem("item").nodeValue();
if (plugin.startsWith(
"html:plugin:imageslideshowplg*")) {
source["id"] = "slideshow";
settings = parse_slideshow(plugin);
} else if (plugin.startsWith("html:plugin:titleplg")) {
source["id"] = "text_gdiplus";
settings = parse_text(plugin);
} else if (plugin.startsWith("http")) {
source["id"] = "browser_source";
int end = plugin.indexOf('*');
settings["url"] =
plugin.left(end).toStdString();
}
} else if (type == 11) {
QString id = attr.namedItem("item").nodeValue();
Json source =
get_source_with_id(id.toStdString(), sources);
name = source["name"].string_value();
goto skip;
}
source["settings"] = settings;
sources.push_back(source);
skip:
struct obs_video_info ovi;
obs_get_video_info(&ovi);
int width = ovi.base_width;
int height = ovi.base_height;
double pos_left =
attr.namedItem("pos_left").nodeValue().toDouble();
double pos_right =
attr.namedItem("pos_right").nodeValue().toDouble();
double pos_top =
attr.namedItem("pos_top").nodeValue().toDouble();
double pos_bottom =
attr.namedItem("pos_bottom").nodeValue().toDouble();
bool visible = attr.namedItem("visible").nodeValue() == "1";
Json out_item = Json::object{
{"bounds_type", 2},
{"pos", Json::object{{"x", pos_left * width},
{"y", pos_top * height}}},
{"bounds",
Json::object{{"x", (pos_right - pos_left) * width},
{"y", (pos_bottom - pos_top) * height}}},
{"name", name},
{"visible", visible}};
items.push_back(out_item);
item = item.nextSibling();
}
}
static Json::object parse_scenes(QDomElement &scenes)
{
Json::array sources = Json::array{};
QString first = "";
QDomNode in_scene = scenes.firstChild();
while (!in_scene.isNull()) {
QString type = in_scene.nodeName();
if (type == "placement") {
QDomNamedNodeMap attr = in_scene.attributes();
QString name = attr.namedItem("name").nodeValue();
QString id = attr.namedItem("id").nodeValue();
if (first.isEmpty())
first = name;
Json out = Json::object{
{"id", "scene"},
{"name", name.toStdString().c_str()},
{"src_id", id.toStdString().c_str()}};
sources.push_back(out);
}
in_scene = in_scene.nextSibling();
}
in_scene = scenes.firstChild();
for (size_t i = 0, l = sources.size(); i < l; i++) {
Json::object source = sources[i].object_items();
Json::array items = Json::array{};
QDomNode firstChild = in_scene.firstChild();
parse_items(firstChild, items, sources);
Json settings = Json::object{{"items", items},
{"id_counter", (int)items.size()}};
source["settings"] = settings;
sources[i] = source;
in_scene = in_scene.nextSibling();
}
return Json::object{{"sources", sources},
{"current_scene", first.toStdString()},
{"current_program_scene", first.toStdString()}};
}
int XSplitImporter::ImportScenes(const string &path, string &name,
json11::Json &res)
{
if (name == "")
name = "XSplit Import";
BPtr<char> file_data = os_quick_read_utf8_file(path.c_str());
if (!file_data)
return IMPORTER_FILE_WONT_OPEN;
QDomDocument doc;
doc.setContent(QString(file_data));
QDomElement docElem = doc.documentElement();
Json::object r = parse_scenes(docElem);
r["name"] = name;
res = r;
QDir dir(path.c_str());
TranslateOSStudio(res);
TranslatePaths(res, QDir::cleanPath(dir.filePath("..")).toStdString());
return IMPORTER_SUCCESS;
}
bool XSplitImporter::Check(const string &path)
{
bool check = false;
BPtr<char> file_data = os_quick_read_utf8_file(path.c_str());
if (!file_data)
return false;
string pos = file_data.Get();
string line = ReadLine(pos);
while (!line.empty()) {
if (line.substr(0, 5) == "<?xml") {
line = ReadLine(pos);
} else {
if (line.substr(0, 14) == "<configuration") {
check = true;
}
break;
}
}
return check;
}
OBSImporterFiles XSplitImporter::FindFiles()
{
OBSImporterFiles res;
#ifdef _WIN32
char dst[512];
int found = os_get_program_data_path(
dst, 512, "SplitMediaLabs\\XSplit\\Presentation2.0\\");
if (found == -1)
return res;
os_dir_t *dir = os_opendir(dst);
struct os_dirent *ent;
while ((ent = os_readdir(dir)) != NULL) {
string name = ent->d_name;
if (ent->directory || name[0] == '.')
continue;
if (name == "Placements.bpres") {
string str = dst + name;
res.push_back(str);
break;
}
}
os_closedir(dir);
#endif
return res;
}