multitrack-video-output.cpp 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918
  1. #include "multitrack-video-output.hpp"
  2. #include <util/dstr.hpp>
  3. #include <util/platform.h>
  4. #include <util/profiler.hpp>
  5. #include <util/util.hpp>
  6. #include <obs-frontend-api.h>
  7. #include <obs-app.hpp>
  8. #include <obs.hpp>
  9. #include <remote-text.hpp>
  10. #include <algorithm>
  11. #include <cinttypes>
  12. #include <cmath>
  13. #include <numeric>
  14. #include <optional>
  15. #include <string>
  16. #include <vector>
  17. #include <QAbstractButton>
  18. #include <QMessageBox>
  19. #include <QObject>
  20. #include <QPushButton>
  21. #include <QScopeGuard>
  22. #include <QString>
  23. #include <QThreadPool>
  24. #include <QUrl>
  25. #include <QUrlQuery>
  26. #include <QUuid>
  27. #include <nlohmann/json.hpp>
  28. #include "system-info.hpp"
  29. #include "goliveapi-postdata.hpp"
  30. #include "goliveapi-network.hpp"
  31. #include "multitrack-video-error.hpp"
  32. #include "qt-helpers.hpp"
  33. #include "models/multitrack-video.hpp"
  34. Qt::ConnectionType BlockingConnectionTypeFor(QObject *object)
  35. {
  36. return object->thread() == QThread::currentThread()
  37. ? Qt::DirectConnection
  38. : Qt::BlockingQueuedConnection;
  39. }
  40. bool MultitrackVideoDeveloperModeEnabled()
  41. {
  42. static bool developer_mode = [] {
  43. auto args = qApp->arguments();
  44. for (const auto &arg : args) {
  45. if (arg == "--enable-multitrack-video-dev") {
  46. return true;
  47. }
  48. }
  49. return false;
  50. }();
  51. return developer_mode;
  52. }
  53. static OBSServiceAutoRelease
  54. create_service(const GoLiveApi::Config &go_live_config,
  55. const std::optional<std::string> &rtmp_url,
  56. const QString &in_stream_key)
  57. {
  58. const char *url = nullptr;
  59. QString stream_key = in_stream_key;
  60. const auto &ingest_endpoints = go_live_config.ingest_endpoints;
  61. for (auto &endpoint : ingest_endpoints) {
  62. if (qstrnicmp("RTMP", endpoint.protocol.c_str(), 4))
  63. continue;
  64. url = endpoint.url_template.c_str();
  65. if (endpoint.authentication &&
  66. !endpoint.authentication->empty()) {
  67. blog(LOG_INFO,
  68. "Using stream key supplied by autoconfig");
  69. stream_key = QString::fromStdString(
  70. *endpoint.authentication);
  71. }
  72. break;
  73. }
  74. if (rtmp_url.has_value()) {
  75. // Despite being set by user, it was set to a ""
  76. if (rtmp_url->empty()) {
  77. throw MultitrackVideoError::warning(QTStr(
  78. "FailedToStartStream.NoCustomRTMPURLInSettings"));
  79. }
  80. url = rtmp_url->c_str();
  81. blog(LOG_INFO, "Using custom RTMP URL: '%s'", url);
  82. } else {
  83. if (!url) {
  84. blog(LOG_ERROR, "No RTMP URL in go live config");
  85. throw MultitrackVideoError::warning(
  86. QTStr("FailedToStartStream.NoRTMPURLInConfig"));
  87. }
  88. blog(LOG_INFO, "Using URL template: '%s'", url);
  89. }
  90. DStr str;
  91. dstr_cat(str, url);
  92. // dstr_find does not protect against null, and dstr_cat will
  93. // not initialize str if cat'ing with a null url
  94. if (!dstr_is_empty(str)) {
  95. auto found = dstr_find(str, "/{stream_key}");
  96. if (found)
  97. dstr_remove(str, found - str->array,
  98. str->len - (found - str->array));
  99. }
  100. /* The stream key itself may contain query parameters, such as
  101. * "bandwidthtest" that need to be carried over. */
  102. QUrl parsed_user_key{in_stream_key};
  103. QUrlQuery user_key_query{parsed_user_key};
  104. QUrl parsed_key{stream_key};
  105. QUrl parsed_url{url};
  106. QUrlQuery parsed_query{parsed_url};
  107. for (const auto &[key, value] : user_key_query.queryItems())
  108. parsed_query.addQueryItem(key, value);
  109. if (!go_live_config.meta.config_id.empty()) {
  110. parsed_query.addQueryItem(
  111. "clientConfigId",
  112. QString::fromStdString(go_live_config.meta.config_id));
  113. }
  114. parsed_key.setQuery(parsed_query);
  115. OBSDataAutoRelease settings = obs_data_create();
  116. obs_data_set_string(settings, "server", str->array);
  117. obs_data_set_string(settings, "key",
  118. parsed_key.toString().toUtf8().constData());
  119. auto service = obs_service_create(
  120. "rtmp_custom", "multitrack video service", settings, nullptr);
  121. if (!service) {
  122. blog(LOG_WARNING, "Failed to create multitrack video service");
  123. throw MultitrackVideoError::warning(QTStr(
  124. "FailedToStartStream.FailedToCreateMultitrackVideoService"));
  125. }
  126. return service;
  127. }
  128. static OBSOutputAutoRelease create_output()
  129. {
  130. OBSOutputAutoRelease output = obs_output_create(
  131. "rtmp_output", "rtmp multitrack video", nullptr, nullptr);
  132. if (!output) {
  133. blog(LOG_ERROR,
  134. "Failed to create multitrack video rtmp output");
  135. throw MultitrackVideoError::warning(QTStr(
  136. "FailedToStartStream.FailedToCreateMultitrackVideoOutput"));
  137. }
  138. return output;
  139. }
  140. static OBSOutputAutoRelease create_recording_output(obs_data_t *settings)
  141. {
  142. OBSOutputAutoRelease output;
  143. bool useMP4 = obs_data_get_bool(settings, "use_mp4");
  144. if (useMP4) {
  145. output = obs_output_create("mp4_output", "mp4 multitrack video",
  146. settings, nullptr);
  147. } else {
  148. output = obs_output_create("flv_output", "flv multitrack video",
  149. settings, nullptr);
  150. }
  151. if (!output) {
  152. blog(LOG_ERROR, "Failed to create multitrack video %s output",
  153. useMP4 ? "mp4" : "flv");
  154. }
  155. return output;
  156. }
  157. static void adjust_video_encoder_scaling(
  158. const obs_video_info &ovi, obs_encoder_t *video_encoder,
  159. const GoLiveApi::VideoEncoderConfiguration &encoder_config,
  160. size_t encoder_index)
  161. {
  162. auto requested_width = encoder_config.width;
  163. auto requested_height = encoder_config.height;
  164. if (ovi.output_width == requested_width ||
  165. ovi.output_height == requested_height)
  166. return;
  167. if (ovi.base_width < requested_width ||
  168. ovi.base_height < requested_height) {
  169. blog(LOG_WARNING,
  170. "Requested resolution exceeds canvas/available resolution for encoder %zu: %" PRIu32
  171. "x%" PRIu32 " > %" PRIu32 "x%" PRIu32,
  172. encoder_index, requested_width, requested_height,
  173. ovi.base_width, ovi.base_height);
  174. }
  175. obs_encoder_set_scaled_size(video_encoder, requested_width,
  176. requested_height);
  177. obs_encoder_set_gpu_scale_type(
  178. video_encoder,
  179. encoder_config.gpu_scale_type.value_or(OBS_SCALE_BICUBIC));
  180. }
  181. static uint32_t closest_divisor(const obs_video_info &ovi,
  182. const media_frames_per_second &target_fps)
  183. {
  184. auto target = (uint64_t)target_fps.numerator * ovi.fps_den;
  185. auto source = (uint64_t)ovi.fps_num * target_fps.denominator;
  186. return std::max(1u, static_cast<uint32_t>(source / target));
  187. }
  188. static void adjust_encoder_frame_rate_divisor(
  189. const obs_video_info &ovi, obs_encoder_t *video_encoder,
  190. const GoLiveApi::VideoEncoderConfiguration &encoder_config,
  191. const size_t encoder_index)
  192. {
  193. if (!encoder_config.framerate) {
  194. blog(LOG_WARNING, "`framerate` not specified for encoder %zu",
  195. encoder_index);
  196. return;
  197. }
  198. media_frames_per_second requested_fps = *encoder_config.framerate;
  199. if (ovi.fps_num == requested_fps.numerator &&
  200. ovi.fps_den == requested_fps.denominator)
  201. return;
  202. auto divisor = closest_divisor(ovi, requested_fps);
  203. if (divisor <= 1)
  204. return;
  205. blog(LOG_INFO, "Setting frame rate divisor to %u for encoder %zu",
  206. divisor, encoder_index);
  207. obs_encoder_set_frame_rate_divisor(video_encoder, divisor);
  208. }
  209. static bool encoder_available(const char *type)
  210. {
  211. const char *id = nullptr;
  212. for (size_t idx = 0; obs_enum_encoder_types(idx, &id); idx++) {
  213. if (strcmp(id, type) == 0)
  214. return true;
  215. }
  216. return false;
  217. }
  218. static OBSEncoderAutoRelease
  219. create_video_encoder(DStr &name_buffer, size_t encoder_index,
  220. const GoLiveApi::VideoEncoderConfiguration &encoder_config)
  221. {
  222. auto encoder_type = encoder_config.type.c_str();
  223. if (!encoder_available(encoder_type)) {
  224. blog(LOG_ERROR, "Encoder type '%s' not available",
  225. encoder_type);
  226. throw MultitrackVideoError::warning(
  227. QTStr("FailedToStartStream.EncoderNotAvailable")
  228. .arg(encoder_type));
  229. }
  230. dstr_printf(name_buffer, "multitrack video video encoder %zu",
  231. encoder_index);
  232. OBSDataAutoRelease encoder_settings = obs_data_create_from_json(
  233. encoder_config.settings.dump().c_str());
  234. obs_data_set_bool(encoder_settings, "disable_scenecut", true);
  235. OBSEncoderAutoRelease video_encoder = obs_video_encoder_create(
  236. encoder_type, name_buffer, encoder_settings, nullptr);
  237. if (!video_encoder) {
  238. blog(LOG_ERROR, "Failed to create video encoder '%s'",
  239. name_buffer->array);
  240. throw MultitrackVideoError::warning(
  241. QTStr("FailedToStartStream.FailedToCreateVideoEncoder")
  242. .arg(name_buffer->array, encoder_type));
  243. }
  244. obs_encoder_set_video(video_encoder, obs_get_video());
  245. obs_video_info ovi;
  246. if (!obs_get_video_info(&ovi)) {
  247. blog(LOG_WARNING,
  248. "Failed to get obs_video_info while creating encoder %zu",
  249. encoder_index);
  250. throw MultitrackVideoError::warning(
  251. QTStr("FailedToStartStream.FailedToGetOBSVideoInfo")
  252. .arg(name_buffer->array, encoder_type));
  253. }
  254. adjust_video_encoder_scaling(ovi, video_encoder, encoder_config,
  255. encoder_index);
  256. adjust_encoder_frame_rate_divisor(ovi, video_encoder, encoder_config,
  257. encoder_index);
  258. return video_encoder;
  259. }
  260. static OBSEncoderAutoRelease create_audio_encoder(const char *name,
  261. const char *audio_encoder_id,
  262. obs_data_t *settings,
  263. size_t mixer_idx)
  264. {
  265. OBSEncoderAutoRelease audio_encoder = obs_audio_encoder_create(
  266. audio_encoder_id, name, settings, mixer_idx, nullptr);
  267. if (!audio_encoder) {
  268. blog(LOG_ERROR, "Failed to create audio encoder");
  269. throw MultitrackVideoError::warning(QTStr(
  270. "FailedToStartStream.FailedToCreateAudioEncoder"));
  271. }
  272. obs_encoder_set_audio(audio_encoder, obs_get_audio());
  273. return audio_encoder;
  274. }
  275. struct OBSOutputs {
  276. OBSOutputAutoRelease output, recording_output;
  277. };
  278. static OBSOutputs
  279. SetupOBSOutput(obs_data_t *dump_stream_to_file_config,
  280. const GoLiveApi::Config &go_live_config,
  281. std::vector<OBSEncoderAutoRelease> &audio_encoders,
  282. std::shared_ptr<obs_encoder_group_t> &video_encoder_group,
  283. const char *audio_encoder_id,
  284. std::optional<size_t> vod_track_mixer);
  285. static void SetupSignalHandlers(bool recording, MultitrackVideoOutput *self,
  286. obs_output_t *output, OBSSignal &start,
  287. OBSSignal &stop, OBSSignal &deactivate);
  288. void MultitrackVideoOutput::PrepareStreaming(
  289. QWidget *parent, const char *service_name, obs_service_t *service,
  290. const std::optional<std::string> &rtmp_url, const QString &stream_key,
  291. const char *audio_encoder_id,
  292. std::optional<uint32_t> maximum_aggregate_bitrate,
  293. std::optional<uint32_t> maximum_video_tracks,
  294. std::optional<std::string> custom_config,
  295. obs_data_t *dump_stream_to_file_config,
  296. std::optional<size_t> vod_track_mixer)
  297. {
  298. {
  299. const std::lock_guard<std::mutex> current_lock{current_mutex};
  300. const std::lock_guard<std::mutex> current_stream_dump_lock{
  301. current_stream_dump_mutex};
  302. if (current || current_stream_dump) {
  303. blog(LOG_WARNING,
  304. "Tried to prepare multitrack video output while it's already active");
  305. return;
  306. }
  307. }
  308. std::optional<GoLiveApi::Config> go_live_config;
  309. std::optional<GoLiveApi::Config> custom;
  310. bool is_custom_config = custom_config.has_value();
  311. auto auto_config_url = MultitrackVideoAutoConfigURL(service);
  312. OBSDataAutoRelease service_settings = obs_service_get_settings(service);
  313. auto multitrack_video_name =
  314. QTStr("Basic.Settings.Stream.MultitrackVideoLabel");
  315. if (obs_data_has_user_value(service_settings,
  316. "multitrack_video_name")) {
  317. multitrack_video_name = obs_data_get_string(
  318. service_settings, "multitrack_video_name");
  319. }
  320. auto auto_config_url_data = auto_config_url.toUtf8();
  321. DStr vod_track_info_storage;
  322. if (vod_track_mixer.has_value())
  323. dstr_printf(vod_track_info_storage, "Yes (mixer: %zu)",
  324. vod_track_mixer.value());
  325. blog(LOG_INFO,
  326. "Preparing enhanced broadcasting stream for:\n"
  327. " custom config: %s\n"
  328. " config url: %s\n"
  329. " settings:\n"
  330. " service: %s\n"
  331. " max aggregate bitrate: %s (%" PRIu32 ")\n"
  332. " max video tracks: %s (%" PRIu32 ")\n"
  333. " custom rtmp url: %s ('%s')\n"
  334. " vod track: %s",
  335. is_custom_config ? "Yes" : "No",
  336. !auto_config_url.isEmpty() ? auto_config_url_data.constData()
  337. : "(null)",
  338. service_name,
  339. maximum_aggregate_bitrate.has_value() ? "Set" : "Auto",
  340. maximum_aggregate_bitrate.value_or(0),
  341. maximum_video_tracks.has_value() ? "Set" : "Auto",
  342. maximum_video_tracks.value_or(0),
  343. rtmp_url.has_value() ? "Yes" : "No",
  344. rtmp_url.has_value() ? rtmp_url->c_str() : "",
  345. vod_track_info_storage->array ? vod_track_info_storage->array
  346. : "No");
  347. const bool custom_config_only =
  348. auto_config_url.isEmpty() &&
  349. MultitrackVideoDeveloperModeEnabled() &&
  350. custom_config.has_value() &&
  351. strcmp(obs_service_get_id(service), "rtmp_custom") == 0;
  352. if (!custom_config_only) {
  353. auto go_live_post = constructGoLivePost(
  354. stream_key, maximum_aggregate_bitrate,
  355. maximum_video_tracks, vod_track_mixer.has_value());
  356. go_live_config = DownloadGoLiveConfig(parent, auto_config_url,
  357. go_live_post,
  358. multitrack_video_name);
  359. }
  360. if (custom_config.has_value()) {
  361. GoLiveApi::Config parsed_custom;
  362. try {
  363. parsed_custom = nlohmann::json::parse(*custom_config);
  364. } catch (const nlohmann::json::exception &exception) {
  365. blog(LOG_WARNING, "Failed to parse custom config: %s",
  366. exception.what());
  367. throw MultitrackVideoError::critical(QTStr(
  368. "FailedToStartStream.InvalidCustomConfig"));
  369. }
  370. // copy unique ID from go live request
  371. if (go_live_config.has_value()) {
  372. parsed_custom.meta.config_id =
  373. go_live_config->meta.config_id;
  374. blog(LOG_INFO,
  375. "Using config_id from go live config with custom config: %s",
  376. parsed_custom.meta.config_id.c_str());
  377. }
  378. nlohmann::json custom_data = parsed_custom;
  379. blog(LOG_INFO, "Using custom go live config: %s",
  380. custom_data.dump(4).c_str());
  381. custom.emplace(std::move(parsed_custom));
  382. }
  383. if (go_live_config.has_value()) {
  384. blog(LOG_INFO, "Enhanced broadcasting config_id: '%s'",
  385. go_live_config->meta.config_id.c_str());
  386. }
  387. if (!go_live_config && !custom) {
  388. blog(LOG_ERROR,
  389. "MultitrackVideoOutput: no config set, this should never happen");
  390. throw MultitrackVideoError::warning(
  391. QTStr("FailedToStartStream.NoConfig"));
  392. }
  393. const auto &output_config = custom ? *custom : *go_live_config;
  394. const auto &service_config = go_live_config ? *go_live_config : *custom;
  395. std::vector<OBSEncoderAutoRelease> audio_encoders;
  396. std::shared_ptr<obs_encoder_group_t> video_encoder_group;
  397. auto outputs = SetupOBSOutput(dump_stream_to_file_config, output_config,
  398. audio_encoders, video_encoder_group,
  399. audio_encoder_id, vod_track_mixer);
  400. auto output = std::move(outputs.output);
  401. auto recording_output = std::move(outputs.recording_output);
  402. if (!output)
  403. throw MultitrackVideoError::warning(
  404. QTStr("FailedToStartStream.FallbackToDefault")
  405. .arg(multitrack_video_name));
  406. auto multitrack_video_service =
  407. create_service(service_config, rtmp_url, stream_key);
  408. if (!multitrack_video_service)
  409. throw MultitrackVideoError::warning(
  410. QTStr("FailedToStartStream.FallbackToDefault")
  411. .arg(multitrack_video_name));
  412. obs_output_set_service(output, multitrack_video_service);
  413. OBSSignal start_streaming;
  414. OBSSignal stop_streaming;
  415. OBSSignal deactivate_stream;
  416. SetupSignalHandlers(false, this, output, start_streaming,
  417. stop_streaming, deactivate_stream);
  418. if (dump_stream_to_file_config && recording_output) {
  419. OBSSignal start_recording;
  420. OBSSignal stop_recording;
  421. OBSSignal deactivate_recording;
  422. SetupSignalHandlers(true, this, recording_output,
  423. start_recording, stop_recording,
  424. deactivate_recording);
  425. decltype(audio_encoders) recording_audio_encoders;
  426. recording_audio_encoders.reserve(audio_encoders.size());
  427. for (auto &encoder : audio_encoders) {
  428. recording_audio_encoders.emplace_back(
  429. obs_encoder_get_ref(encoder));
  430. }
  431. {
  432. const std::lock_guard current_stream_dump_lock{
  433. current_stream_dump_mutex};
  434. current_stream_dump.emplace(OBSOutputObjects{
  435. std::move(recording_output),
  436. video_encoder_group,
  437. std::move(recording_audio_encoders),
  438. nullptr,
  439. std::move(start_recording),
  440. std::move(stop_recording),
  441. std::move(deactivate_recording),
  442. });
  443. }
  444. }
  445. const std::lock_guard current_lock{current_mutex};
  446. current.emplace(OBSOutputObjects{
  447. std::move(output),
  448. video_encoder_group,
  449. std::move(audio_encoders),
  450. std::move(multitrack_video_service),
  451. std::move(start_streaming),
  452. std::move(stop_streaming),
  453. std::move(deactivate_stream),
  454. });
  455. }
  456. signal_handler_t *MultitrackVideoOutput::StreamingSignalHandler()
  457. {
  458. const std::lock_guard current_lock{current_mutex};
  459. return current.has_value()
  460. ? obs_output_get_signal_handler(current->output_)
  461. : nullptr;
  462. }
  463. void MultitrackVideoOutput::StartedStreaming()
  464. {
  465. OBSOutputAutoRelease dump_output;
  466. {
  467. const std::lock_guard current_stream_dump_lock{
  468. current_stream_dump_mutex};
  469. if (current_stream_dump && current_stream_dump->output_) {
  470. dump_output = obs_output_get_ref(
  471. current_stream_dump->output_);
  472. }
  473. }
  474. if (!dump_output)
  475. return;
  476. auto result = obs_output_start(dump_output);
  477. blog(LOG_INFO, "MultitrackVideoOutput: starting recording%s",
  478. result ? "" : " failed");
  479. }
  480. void MultitrackVideoOutput::StopStreaming()
  481. {
  482. OBSOutputAutoRelease current_output;
  483. {
  484. const std::lock_guard current_lock{current_mutex};
  485. if (current && current->output_)
  486. current_output = obs_output_get_ref(current->output_);
  487. }
  488. if (current_output)
  489. obs_output_stop(current_output);
  490. OBSOutputAutoRelease dump_output;
  491. {
  492. const std::lock_guard current_stream_dump_lock{
  493. current_stream_dump_mutex};
  494. if (current_stream_dump && current_stream_dump->output_)
  495. dump_output = obs_output_get_ref(
  496. current_stream_dump->output_);
  497. }
  498. if (dump_output)
  499. obs_output_stop(dump_output);
  500. }
  501. bool MultitrackVideoOutput::HandleIncompatibleSettings(
  502. QWidget *parent, config_t *config, obs_service_t *service,
  503. bool &useDelay, bool &enableNewSocketLoop, bool &enableDynBitrate)
  504. {
  505. QString incompatible_settings;
  506. QString where_to_disable;
  507. QString incompatible_settings_list;
  508. size_t num = 1;
  509. auto check_setting = [&](bool setting, const char *name,
  510. const char *section) {
  511. if (!setting)
  512. return;
  513. incompatible_settings +=
  514. QString(" %1. %2\n").arg(num).arg(QTStr(name));
  515. where_to_disable +=
  516. QString(" %1. [%2 → %3 → %4]\n")
  517. .arg(num)
  518. .arg(QTStr("Settings"))
  519. .arg(QTStr("Basic.Settings.Advanced"))
  520. .arg(QTStr(section));
  521. incompatible_settings_list += QString("%1, ").arg(name);
  522. num += 1;
  523. };
  524. check_setting(useDelay, "Basic.Settings.Advanced.StreamDelay",
  525. "Basic.Settings.Advanced.StreamDelay");
  526. #ifdef _WIN32
  527. check_setting(enableNewSocketLoop,
  528. "Basic.Settings.Advanced.Network.EnableNewSocketLoop",
  529. "Basic.Settings.Advanced.Network");
  530. #endif
  531. check_setting(enableDynBitrate,
  532. "Basic.Settings.Output.DynamicBitrate.Beta",
  533. "Basic.Settings.Advanced.Network");
  534. if (incompatible_settings.isEmpty())
  535. return true;
  536. OBSDataAutoRelease service_settings = obs_service_get_settings(service);
  537. QMessageBox mb(parent);
  538. mb.setIcon(QMessageBox::Critical);
  539. mb.setWindowTitle(QTStr("MultitrackVideo.IncompatibleSettings.Title"));
  540. mb.setText(QString(QTStr("MultitrackVideo.IncompatibleSettings.Text"))
  541. .arg(obs_data_get_string(service_settings,
  542. "multitrack_video_name"))
  543. .arg(incompatible_settings)
  544. .arg(where_to_disable));
  545. auto this_stream = mb.addButton(
  546. QTStr("MultitrackVideo.IncompatibleSettings.DisableAndStartStreaming"),
  547. QMessageBox::AcceptRole);
  548. auto all_streams = mb.addButton(
  549. QString(QTStr(
  550. "MultitrackVideo.IncompatibleSettings.UpdateAndStartStreaming")),
  551. QMessageBox::AcceptRole);
  552. mb.setStandardButtons(QMessageBox::StandardButton::Cancel);
  553. mb.exec();
  554. const char *action = "cancel";
  555. if (mb.clickedButton() == this_stream) {
  556. action = "DisableAndStartStreaming";
  557. } else if (mb.clickedButton() == all_streams) {
  558. action = "UpdateAndStartStreaming";
  559. }
  560. blog(LOG_INFO,
  561. "MultitrackVideoOutput: attempted to start stream with incompatible"
  562. "settings (%s); action taken: %s",
  563. incompatible_settings_list.toUtf8().constData(), action);
  564. if (mb.clickedButton() == this_stream ||
  565. mb.clickedButton() == all_streams) {
  566. useDelay = false;
  567. enableNewSocketLoop = false;
  568. enableDynBitrate = false;
  569. if (mb.clickedButton() == all_streams) {
  570. config_set_bool(config, "Output", "DelayEnable", false);
  571. #ifdef _WIN32
  572. config_set_bool(config, "Output", "NewSocketLoopEnable",
  573. false);
  574. #endif
  575. config_set_bool(config, "Output", "DynamicBitrate",
  576. false);
  577. }
  578. return true;
  579. }
  580. MultitrackVideoOutput::ReleaseOnMainThread(take_current());
  581. MultitrackVideoOutput::ReleaseOnMainThread(take_current_stream_dump());
  582. return false;
  583. }
  584. static bool
  585. create_video_encoders(const GoLiveApi::Config &go_live_config,
  586. std::shared_ptr<obs_encoder_group_t> &video_encoder_group,
  587. obs_output_t *output, obs_output_t *recording_output)
  588. {
  589. DStr video_encoder_name_buffer;
  590. if (go_live_config.encoder_configurations.empty()) {
  591. blog(LOG_WARNING,
  592. "MultitrackVideoOutput: Missing video encoder configurations");
  593. throw MultitrackVideoError::warning(
  594. QTStr("FailedToStartStream.MissingEncoderConfigs"));
  595. }
  596. std::shared_ptr<obs_encoder_group_t> encoder_group(
  597. obs_encoder_group_create(), obs_encoder_group_destroy);
  598. if (!encoder_group)
  599. return false;
  600. for (size_t i = 0; i < go_live_config.encoder_configurations.size();
  601. i++) {
  602. auto encoder = create_video_encoder(
  603. video_encoder_name_buffer, i,
  604. go_live_config.encoder_configurations[i]);
  605. if (!encoder)
  606. return false;
  607. if (!obs_encoder_set_group(encoder, encoder_group.get()))
  608. return false;
  609. obs_output_set_video_encoder2(output, encoder, i);
  610. if (recording_output)
  611. obs_output_set_video_encoder2(recording_output, encoder,
  612. i);
  613. }
  614. video_encoder_group = encoder_group;
  615. return true;
  616. }
  617. static void
  618. create_audio_encoders(const GoLiveApi::Config &go_live_config,
  619. std::vector<OBSEncoderAutoRelease> &audio_encoders,
  620. obs_output_t *output, obs_output_t *recording_output,
  621. const char *audio_encoder_id,
  622. std::optional<size_t> vod_track_mixer)
  623. {
  624. using encoder_configs_type =
  625. decltype(go_live_config.audio_configurations.live);
  626. DStr encoder_name_buffer;
  627. size_t output_encoder_index = 0;
  628. auto create_encoders = [&](const char *name_prefix,
  629. const encoder_configs_type &configs,
  630. size_t mixer_idx) {
  631. if (configs.empty()) {
  632. blog(LOG_WARNING,
  633. "MultitrackVideoOutput: Missing audio encoder configurations (for '%s')",
  634. name_prefix);
  635. throw MultitrackVideoError::warning(QTStr(
  636. "FailedToStartStream.MissingEncoderConfigs"));
  637. }
  638. for (size_t i = 0; i < configs.size(); i++) {
  639. dstr_printf(encoder_name_buffer, "%s %zu", name_prefix,
  640. i);
  641. OBSDataAutoRelease settings = obs_data_create_from_json(
  642. configs[i].settings.dump().c_str());
  643. OBSEncoderAutoRelease audio_encoder =
  644. create_audio_encoder(encoder_name_buffer->array,
  645. audio_encoder_id, settings,
  646. mixer_idx);
  647. obs_output_set_audio_encoder(output, audio_encoder,
  648. output_encoder_index);
  649. if (recording_output)
  650. obs_output_set_audio_encoder(
  651. recording_output, audio_encoder,
  652. output_encoder_index);
  653. output_encoder_index += 1;
  654. audio_encoders.emplace_back(std::move(audio_encoder));
  655. }
  656. };
  657. create_encoders("multitrack video live audio",
  658. go_live_config.audio_configurations.live, 0);
  659. if (!vod_track_mixer.has_value())
  660. return;
  661. // we already check for empty inside of `create_encoders`
  662. encoder_configs_type empty = {};
  663. create_encoders("multitrack video vod audio",
  664. go_live_config.audio_configurations.vod.value_or(empty),
  665. *vod_track_mixer);
  666. return;
  667. }
  668. static OBSOutputs
  669. SetupOBSOutput(obs_data_t *dump_stream_to_file_config,
  670. const GoLiveApi::Config &go_live_config,
  671. std::vector<OBSEncoderAutoRelease> &audio_encoders,
  672. std::shared_ptr<obs_encoder_group_t> &video_encoder_group,
  673. const char *audio_encoder_id,
  674. std::optional<size_t> vod_track_mixer)
  675. {
  676. auto output = create_output();
  677. OBSOutputAutoRelease recording_output;
  678. if (dump_stream_to_file_config)
  679. recording_output =
  680. create_recording_output(dump_stream_to_file_config);
  681. if (!create_video_encoders(go_live_config, video_encoder_group, output,
  682. recording_output))
  683. return {nullptr, nullptr};
  684. create_audio_encoders(go_live_config, audio_encoders, output,
  685. recording_output, audio_encoder_id,
  686. vod_track_mixer);
  687. return {std::move(output), std::move(recording_output)};
  688. }
  689. void SetupSignalHandlers(bool recording, MultitrackVideoOutput *self,
  690. obs_output_t *output, OBSSignal &start,
  691. OBSSignal &stop, OBSSignal &deactivate)
  692. {
  693. auto handler = obs_output_get_signal_handler(output);
  694. if (recording)
  695. start.Connect(handler, "start", RecordingStartHandler, self);
  696. stop.Connect(handler, "stop",
  697. !recording ? StreamStopHandler : RecordingStopHandler,
  698. self);
  699. deactivate.Connect(handler, "deactivate",
  700. !recording ? StreamDeactivateHandler
  701. : RecordingDeactivateHandler,
  702. self);
  703. }
  704. std::optional<MultitrackVideoOutput::OBSOutputObjects>
  705. MultitrackVideoOutput::take_current()
  706. {
  707. const std::lock_guard<std::mutex> current_lock{current_mutex};
  708. auto val = std::move(current);
  709. current.reset();
  710. return val;
  711. }
  712. std::optional<MultitrackVideoOutput::OBSOutputObjects>
  713. MultitrackVideoOutput::take_current_stream_dump()
  714. {
  715. const std::lock_guard<std::mutex> current_stream_dump_lock{
  716. current_stream_dump_mutex};
  717. auto val = std::move(current_stream_dump);
  718. current_stream_dump.reset();
  719. return val;
  720. }
  721. void MultitrackVideoOutput::ReleaseOnMainThread(
  722. std::optional<OBSOutputObjects> objects)
  723. {
  724. if (!objects.has_value())
  725. return;
  726. QMetaObject::invokeMethod(
  727. QApplication::instance()->thread(),
  728. [objects = std::move(objects)] {}, Qt::QueuedConnection);
  729. }
  730. void StreamStopHandler(void *arg, calldata_t *params)
  731. {
  732. auto self = static_cast<MultitrackVideoOutput *>(arg);
  733. OBSOutputAutoRelease stream_dump_output;
  734. {
  735. const std::lock_guard<std::mutex> current_stream_dump_lock{
  736. self->current_stream_dump_mutex};
  737. if (self->current_stream_dump &&
  738. self->current_stream_dump->output_)
  739. stream_dump_output = obs_output_get_ref(
  740. self->current_stream_dump->output_);
  741. }
  742. if (stream_dump_output)
  743. obs_output_stop(stream_dump_output);
  744. if (obs_output_active(static_cast<obs_output_t *>(
  745. calldata_ptr(params, "output"))))
  746. return;
  747. MultitrackVideoOutput::ReleaseOnMainThread(self->take_current());
  748. }
  749. void StreamDeactivateHandler(void *arg, calldata_t *params)
  750. {
  751. auto self = static_cast<MultitrackVideoOutput *>(arg);
  752. if (obs_output_reconnecting(static_cast<obs_output_t *>(
  753. calldata_ptr(params, "output"))))
  754. return;
  755. MultitrackVideoOutput::ReleaseOnMainThread(self->take_current());
  756. }
  757. void RecordingStartHandler(void * /* arg */, calldata_t * /* data */)
  758. {
  759. blog(LOG_INFO, "MultitrackVideoOutput: recording started");
  760. }
  761. void RecordingStopHandler(void *arg, calldata_t *params)
  762. {
  763. auto self = static_cast<MultitrackVideoOutput *>(arg);
  764. blog(LOG_INFO, "MultitrackVideoOutput: recording stopped");
  765. if (obs_output_active(static_cast<obs_output_t *>(
  766. calldata_ptr(params, "output"))))
  767. return;
  768. MultitrackVideoOutput::ReleaseOnMainThread(
  769. self->take_current_stream_dump());
  770. }
  771. void RecordingDeactivateHandler(void *arg, calldata_t * /*data*/)
  772. {
  773. auto self = static_cast<MultitrackVideoOutput *>(arg);
  774. MultitrackVideoOutput::ReleaseOnMainThread(
  775. self->take_current_stream_dump());
  776. }