xsplit.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. /******************************************************************************
  2. Copyright (C) 2019-2020 by Dillon Pentz <[email protected]>
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU General Public License as published by
  5. the Free Software Foundation, either version 2 of the License, or
  6. (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU General Public License for more details.
  11. You should have received a copy of the GNU General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. ******************************************************************************/
  14. #include "importers.hpp"
  15. #include <ctype.h>
  16. #include <QDomDocument>
  17. using namespace std;
  18. using namespace json11;
  19. static int hex_string_to_int(string str)
  20. {
  21. int res = 0;
  22. if (str[0] == '#')
  23. str = str.substr(1);
  24. for (size_t i = 0, l = str.size(); i < l; i++) {
  25. res *= 16;
  26. if (str[0] >= '0' && str[0] <= '9')
  27. res += str[0] - '0';
  28. else
  29. res += str[0] - 'A' + 10;
  30. str = str.substr(1);
  31. }
  32. return res;
  33. }
  34. static Json::object parse_text(QString &config)
  35. {
  36. int start = config.indexOf("*{");
  37. config = config.mid(start + 1);
  38. config.replace("\\", "/");
  39. string err;
  40. Json data = Json::parse(config.toStdString(), err);
  41. if (err != "")
  42. return Json::object{};
  43. string outline = data["outline"].string_value();
  44. int out = 0;
  45. if (outline == "thick")
  46. out = 20;
  47. else if (outline == "thicker")
  48. out = 40;
  49. else if (outline == "thinner")
  50. out = 5;
  51. else if (outline == "thin")
  52. out = 10;
  53. string valign = data["vertAlign"].string_value();
  54. if (valign == "middle")
  55. valign = "center";
  56. Json font = Json::object{{"face", data["fontStyle"]}, {"size", 200}};
  57. return Json::object{
  58. {"text", data["text"]},
  59. {"font", font},
  60. {"outline", out > 0},
  61. {"outline_size", out},
  62. {"outline_color",
  63. hex_string_to_int(data["outlineColor"].string_value())},
  64. {"color", hex_string_to_int(data["color"].string_value())},
  65. {"align", data["textAlign"]},
  66. {"valign", valign},
  67. {"alpha", data["opacity"]}};
  68. }
  69. static Json::array parse_playlist(QString &playlist)
  70. {
  71. Json::array out = Json::array{};
  72. while (true) {
  73. int end = playlist.indexOf('*');
  74. QString path = playlist.left(end);
  75. out.push_back(Json::object{{"value", path.toStdString()}});
  76. int next = playlist.indexOf('|');
  77. if (next == -1)
  78. break;
  79. playlist = playlist.mid(next + 1);
  80. }
  81. return out;
  82. }
  83. static void parse_media_types(QDomNamedNodeMap &attr, Json::object &source,
  84. Json::object &settings)
  85. {
  86. QString playlist = attr.namedItem("FilePlaylist").nodeValue();
  87. if (playlist != "") {
  88. source["id"] = "vlc_source";
  89. settings["playlist"] = parse_playlist(playlist);
  90. QString end_op = attr.namedItem("OpWhenFinished").nodeValue();
  91. if (end_op == "2")
  92. settings["loop"] = true;
  93. } else {
  94. QString url = attr.namedItem("item").nodeValue();
  95. int sep = url.indexOf("://");
  96. if (sep != -1) {
  97. QString prot = url.left(sep);
  98. if (prot == "smlndi") {
  99. source["id"] = "ndi_source";
  100. } else {
  101. source["id"] = "ffmpeg_source";
  102. int info = url.indexOf("\\");
  103. QString input;
  104. if (info != -1) {
  105. input = url.left(info);
  106. } else {
  107. input = url;
  108. }
  109. settings["input"] = input.toStdString();
  110. settings["is_local_file"] = false;
  111. }
  112. } else {
  113. source["id"] = "ffmpeg_source";
  114. settings["local_file"] =
  115. url.replace("\\", "/").toStdString();
  116. settings["is_local_file"] = true;
  117. }
  118. }
  119. }
  120. static Json::object parse_slideshow(QString &config)
  121. {
  122. int start = config.indexOf("images\":[");
  123. if (start == -1)
  124. return Json::object{};
  125. config = config.mid(start + 8);
  126. config.replace("\\\\", "/");
  127. int end = config.indexOf(']');
  128. if (end == -1)
  129. return Json::object{};
  130. string arr = config.left(end + 1).toStdString();
  131. string err;
  132. Json::array files = Json::parse(arr, err).array_items();
  133. if (err != "")
  134. return Json::object{};
  135. Json::array files_out = Json::array{};
  136. for (size_t i = 0; i < files.size(); i++) {
  137. string file = files[i].string_value();
  138. files_out.push_back(Json::object{{"value", file}});
  139. }
  140. QString options = config.mid(end + 1);
  141. options[0] = '{';
  142. Json opt = Json::parse(options.toStdString(), err);
  143. if (err != "")
  144. return Json::object{};
  145. return Json::object{{"randomize", opt["random"]},
  146. {"slide_time",
  147. opt["delay"].number_value() * 1000 + 700},
  148. {"files", files_out}};
  149. }
  150. static bool source_name_exists(const string &name, const Json::array &sources)
  151. {
  152. for (size_t i = 0; i < sources.size(); i++) {
  153. if (sources.at(i)["name"].string_value() == name)
  154. return true;
  155. }
  156. return false;
  157. }
  158. static Json get_source_with_id(const string &src_id, const Json::array &sources)
  159. {
  160. for (size_t i = 0; i < sources.size(); i++) {
  161. if (sources.at(i)["src_id"].string_value() == src_id)
  162. return sources.at(i);
  163. }
  164. return nullptr;
  165. }
  166. static void parse_items(QDomNode &item, Json::array &items,
  167. Json::array &sources)
  168. {
  169. while (!item.isNull()) {
  170. QDomNamedNodeMap attr = item.attributes();
  171. QString srcid = attr.namedItem("srcid").nodeValue();
  172. double vol = attr.namedItem("volume").nodeValue().toDouble();
  173. int type = attr.namedItem("type").nodeValue().toInt();
  174. string name;
  175. Json::object settings;
  176. Json::object source;
  177. string temp_name;
  178. int x = 0;
  179. Json exists = get_source_with_id(srcid.toStdString(), sources);
  180. if (!exists.is_null()) {
  181. name = exists["name"].string_value();
  182. goto skip;
  183. }
  184. name = attr.namedItem("cname").nodeValue().toStdString();
  185. if (name.empty() || name[0] == '\0')
  186. name = attr.namedItem("name").nodeValue().toStdString();
  187. temp_name = name;
  188. while (source_name_exists(temp_name, sources)) {
  189. string new_name = name + " " + to_string(x++);
  190. temp_name = new_name;
  191. }
  192. name = temp_name;
  193. settings = Json::object{};
  194. source = Json::object{{"name", name},
  195. {"src_id", srcid.toStdString()},
  196. {"volume", vol}};
  197. /** type=1 means Media of some kind (Video Playlist, RTSP,
  198. RTMP, NDI or Media File).
  199. type=2 means either a DShow or WASAPI source.
  200. type=4 means an Image source.
  201. type=5 means either a Display or Window Capture.
  202. type=7 means a Game Capture.
  203. type=8 means rendered with a browser, which includes:
  204. Web Page, Image Slideshow, Text.
  205. type=11 means another Scene. **/
  206. if (type == 1) {
  207. parse_media_types(attr, source, settings);
  208. } else if (type == 2) {
  209. QString audio = attr.namedItem("itemaudio").nodeValue();
  210. if (audio.isEmpty()) {
  211. source["id"] = "dshow_input";
  212. } else {
  213. source["id"] = "wasapi_input_capture";
  214. int dev = audio.indexOf("\\wave:") + 6;
  215. QString res =
  216. "{0.0.1.00000000}." + audio.mid(dev);
  217. res = res.toLower();
  218. settings["device_id"] = res.toStdString();
  219. }
  220. } else if (type == 4) {
  221. source["id"] = "image_source";
  222. QString path = attr.namedItem("item").nodeValue();
  223. path.replace("\\", "/");
  224. settings["file"] = path.toStdString();
  225. } else if (type == 5) {
  226. QString opt = attr.namedItem("item").nodeValue();
  227. QDomDocument options;
  228. options.setContent(opt);
  229. QDomNode el = options.documentElement();
  230. QDomNamedNodeMap o_attr = el.attributes();
  231. QString display =
  232. o_attr.namedItem("desktop").nodeValue();
  233. if (!display.isEmpty()) {
  234. source["id"] = "monitor_capture";
  235. int cursor = attr.namedItem("ScrCapShowMouse")
  236. .nodeValue()
  237. .toInt();
  238. settings["capture_cursor"] = cursor == 1;
  239. } else {
  240. source["id"] = "window_capture";
  241. QString exec =
  242. o_attr.namedItem("module").nodeValue();
  243. QString window =
  244. o_attr.namedItem("window").nodeValue();
  245. QString _class =
  246. o_attr.namedItem("class").nodeValue();
  247. int pos = exec.lastIndexOf('\\');
  248. if (_class.isEmpty()) {
  249. _class = "class";
  250. }
  251. QString res = window + ":" + _class + ":" +
  252. exec.mid(pos + 1);
  253. settings["window"] = res.toStdString();
  254. settings["priority"] = 2;
  255. }
  256. } else if (type == 7) {
  257. QString opt = attr.namedItem("item").nodeValue();
  258. opt.replace("&lt;", "<");
  259. opt.replace("&gt;", ">");
  260. opt.replace("&quot;", "\"");
  261. QDomDocument doc;
  262. doc.setContent(opt);
  263. QDomNode el = doc.documentElement();
  264. QDomNamedNodeMap o_attr = el.attributes();
  265. QString name = o_attr.namedItem("wndname").nodeValue();
  266. QString exec =
  267. o_attr.namedItem("imagename").nodeValue();
  268. QString res = name = "::" + exec;
  269. source["id"] = "game_capture";
  270. settings["window"] = res.toStdString();
  271. settings["capture_mode"] = "window";
  272. } else if (type == 8) {
  273. QString plugin = attr.namedItem("item").nodeValue();
  274. if (plugin.startsWith(
  275. "html:plugin:imageslideshowplg*")) {
  276. source["id"] = "slideshow";
  277. settings = parse_slideshow(plugin);
  278. } else if (plugin.startsWith("html:plugin:titleplg")) {
  279. source["id"] = "text_gdiplus";
  280. settings = parse_text(plugin);
  281. } else if (plugin.startsWith("http")) {
  282. source["id"] = "browser_source";
  283. int end = plugin.indexOf('*');
  284. settings["url"] =
  285. plugin.left(end).toStdString();
  286. }
  287. } else if (type == 11) {
  288. QString id = attr.namedItem("item").nodeValue();
  289. Json source =
  290. get_source_with_id(id.toStdString(), sources);
  291. name = source["name"].string_value();
  292. goto skip;
  293. }
  294. source["settings"] = settings;
  295. sources.push_back(source);
  296. skip:
  297. struct obs_video_info ovi;
  298. obs_get_video_info(&ovi);
  299. int width = ovi.base_width;
  300. int height = ovi.base_height;
  301. double pos_left =
  302. attr.namedItem("pos_left").nodeValue().toDouble();
  303. double pos_right =
  304. attr.namedItem("pos_right").nodeValue().toDouble();
  305. double pos_top =
  306. attr.namedItem("pos_top").nodeValue().toDouble();
  307. double pos_bottom =
  308. attr.namedItem("pos_bottom").nodeValue().toDouble();
  309. bool visible = attr.namedItem("visible").nodeValue() == "1";
  310. Json out_item = Json::object{
  311. {"bounds_type", 2},
  312. {"pos", Json::object{{"x", pos_left * width},
  313. {"y", pos_top * height}}},
  314. {"bounds",
  315. Json::object{{"x", (pos_right - pos_left) * width},
  316. {"y", (pos_bottom - pos_top) * height}}},
  317. {"name", name},
  318. {"visible", visible}};
  319. items.push_back(out_item);
  320. item = item.nextSibling();
  321. }
  322. }
  323. static Json::object parse_scenes(QDomElement &scenes)
  324. {
  325. Json::array sources = Json::array{};
  326. QString first = "";
  327. QDomNode in_scene = scenes.firstChild();
  328. while (!in_scene.isNull()) {
  329. QString type = in_scene.nodeName();
  330. if (type == "placement") {
  331. QDomNamedNodeMap attr = in_scene.attributes();
  332. QString name = attr.namedItem("name").nodeValue();
  333. QString id = attr.namedItem("id").nodeValue();
  334. if (first.isEmpty())
  335. first = name;
  336. Json out = Json::object{
  337. {"id", "scene"},
  338. {"name", name.toStdString().c_str()},
  339. {"src_id", id.toStdString().c_str()}};
  340. sources.push_back(out);
  341. }
  342. in_scene = in_scene.nextSibling();
  343. }
  344. in_scene = scenes.firstChild();
  345. for (size_t i = 0, l = sources.size(); i < l; i++) {
  346. Json::object source = sources[i].object_items();
  347. Json::array items = Json::array{};
  348. QDomNode firstChild = in_scene.firstChild();
  349. parse_items(firstChild, items, sources);
  350. Json settings = Json::object{{"items", items},
  351. {"id_counter", (int)items.size()}};
  352. source["settings"] = settings;
  353. sources[i] = source;
  354. in_scene = in_scene.nextSibling();
  355. }
  356. return Json::object{{"sources", sources},
  357. {"current_scene", first.toStdString()},
  358. {"current_program_scene", first.toStdString()}};
  359. }
  360. int XSplitImporter::ImportScenes(const string &path, string &name,
  361. json11::Json &res)
  362. {
  363. if (name == "")
  364. name = "XSplit Import";
  365. BPtr<char> file_data = os_quick_read_utf8_file(path.c_str());
  366. if (!file_data)
  367. return IMPORTER_FILE_WONT_OPEN;
  368. QDomDocument doc;
  369. doc.setContent(QString(file_data));
  370. QDomElement docElem = doc.documentElement();
  371. Json::object r = parse_scenes(docElem);
  372. r["name"] = name;
  373. res = r;
  374. QDir dir(path.c_str());
  375. TranslateOSStudio(res);
  376. TranslatePaths(res, QDir::cleanPath(dir.filePath("..")).toStdString());
  377. return IMPORTER_SUCCESS;
  378. }
  379. bool XSplitImporter::Check(const string &path)
  380. {
  381. bool check = false;
  382. BPtr<char> file_data = os_quick_read_utf8_file(path.c_str());
  383. if (!file_data)
  384. return false;
  385. string pos = file_data.Get();
  386. string line = ReadLine(pos);
  387. while (!line.empty()) {
  388. if (line.substr(0, 5) == "<?xml") {
  389. line = ReadLine(pos);
  390. } else {
  391. if (line.substr(0, 14) == "<configuration") {
  392. check = true;
  393. }
  394. break;
  395. }
  396. }
  397. return check;
  398. }
  399. OBSImporterFiles XSplitImporter::FindFiles()
  400. {
  401. OBSImporterFiles res;
  402. #ifdef _WIN32
  403. char dst[512];
  404. int found = os_get_program_data_path(
  405. dst, 512, "SplitMediaLabs\\XSplit\\Presentation2.0\\");
  406. if (found == -1)
  407. return res;
  408. os_dir_t *dir = os_opendir(dst);
  409. struct os_dirent *ent;
  410. while ((ent = os_readdir(dir)) != NULL) {
  411. string name = ent->d_name;
  412. if (ent->directory || name[0] == '.')
  413. continue;
  414. if (name == "Placements.bpres") {
  415. string str = dst + name;
  416. res.push_back(str);
  417. break;
  418. }
  419. }
  420. os_closedir(dir);
  421. #endif
  422. return res;
  423. }