multitrack-video-output.cpp 30 KB

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