multitrack-video-output.cpp 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042
  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 <algorithm>
  12. #include <cinttypes>
  13. #include <cmath>
  14. #include <numeric>
  15. #include <optional>
  16. #include <string>
  17. #include <vector>
  18. #include <QAbstractButton>
  19. #include <QMessageBox>
  20. #include <QObject>
  21. #include <QPushButton>
  22. #include <QScopeGuard>
  23. #include <QString>
  24. #include <QThreadPool>
  25. #include <QUrl>
  26. #include <QUrlQuery>
  27. #include <QUuid>
  28. #include <nlohmann/json.hpp>
  29. #include "system-info.hpp"
  30. #include "goliveapi-postdata.hpp"
  31. #include "goliveapi-network.hpp"
  32. #include "multitrack-video-error.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(QWidget *parent, const QString &multitrack_video_name,
  280. obs_data_t *dump_stream_to_file_config,
  281. const GoLiveApi::Config &go_live_config,
  282. std::vector<OBSEncoderAutoRelease> &audio_encoders,
  283. std::shared_ptr<obs_encoder_group_t> &video_encoder_group,
  284. const char *audio_encoder_id, size_t main_audio_mixer,
  285. std::optional<size_t> vod_track_mixer);
  286. static void SetupSignalHandlers(bool recording, MultitrackVideoOutput *self,
  287. obs_output_t *output, OBSSignal &start,
  288. OBSSignal &stop, OBSSignal &deactivate);
  289. void MultitrackVideoOutput::PrepareStreaming(
  290. QWidget *parent, const char *service_name, obs_service_t *service,
  291. const std::optional<std::string> &rtmp_url, const QString &stream_key,
  292. const char *audio_encoder_id,
  293. std::optional<uint32_t> maximum_aggregate_bitrate,
  294. std::optional<uint32_t> maximum_video_tracks,
  295. std::optional<std::string> custom_config,
  296. obs_data_t *dump_stream_to_file_config, size_t main_audio_mixer,
  297. std::optional<size_t> vod_track_mixer)
  298. {
  299. {
  300. const std::lock_guard<std::mutex> current_lock{current_mutex};
  301. const std::lock_guard<std::mutex> current_stream_dump_lock{
  302. current_stream_dump_mutex};
  303. if (current || current_stream_dump) {
  304. blog(LOG_WARNING,
  305. "Tried to prepare multitrack video output while it's already active");
  306. return;
  307. }
  308. }
  309. std::optional<GoLiveApi::Config> go_live_config;
  310. std::optional<GoLiveApi::Config> custom;
  311. bool is_custom_config = custom_config.has_value();
  312. auto auto_config_url = MultitrackVideoAutoConfigURL(service);
  313. OBSDataAutoRelease service_settings = obs_service_get_settings(service);
  314. auto multitrack_video_name =
  315. QTStr("Basic.Settings.Stream.MultitrackVideoLabel");
  316. if (obs_data_has_user_value(service_settings,
  317. "multitrack_video_name")) {
  318. multitrack_video_name = obs_data_get_string(
  319. service_settings, "multitrack_video_name");
  320. }
  321. auto auto_config_url_data = auto_config_url.toUtf8();
  322. DStr vod_track_info_storage;
  323. if (vod_track_mixer.has_value())
  324. dstr_printf(vod_track_info_storage, "Yes (mixer: %zu)",
  325. vod_track_mixer.value());
  326. blog(LOG_INFO,
  327. "Preparing enhanced broadcasting stream for:\n"
  328. " custom config: %s\n"
  329. " config url: %s\n"
  330. " settings:\n"
  331. " service: %s\n"
  332. " max aggregate bitrate: %s (%" PRIu32 ")\n"
  333. " max video tracks: %s (%" PRIu32 ")\n"
  334. " custom rtmp url: %s ('%s')\n"
  335. " vod track: %s",
  336. is_custom_config ? "Yes" : "No",
  337. !auto_config_url.isEmpty() ? auto_config_url_data.constData()
  338. : "(null)",
  339. service_name,
  340. maximum_aggregate_bitrate.has_value() ? "Set" : "Auto",
  341. maximum_aggregate_bitrate.value_or(0),
  342. maximum_video_tracks.has_value() ? "Set" : "Auto",
  343. maximum_video_tracks.value_or(0),
  344. rtmp_url.has_value() ? "Yes" : "No",
  345. rtmp_url.has_value() ? rtmp_url->c_str() : "",
  346. vod_track_info_storage->array ? vod_track_info_storage->array
  347. : "No");
  348. const bool custom_config_only =
  349. auto_config_url.isEmpty() &&
  350. MultitrackVideoDeveloperModeEnabled() &&
  351. custom_config.has_value() &&
  352. strcmp(obs_service_get_id(service), "rtmp_custom") == 0;
  353. if (!custom_config_only) {
  354. auto go_live_post = constructGoLivePost(
  355. stream_key, maximum_aggregate_bitrate,
  356. maximum_video_tracks, vod_track_mixer.has_value());
  357. go_live_config = DownloadGoLiveConfig(parent, auto_config_url,
  358. go_live_post,
  359. multitrack_video_name);
  360. }
  361. if (custom_config.has_value()) {
  362. GoLiveApi::Config parsed_custom;
  363. try {
  364. parsed_custom = nlohmann::json::parse(*custom_config);
  365. } catch (const nlohmann::json::exception &exception) {
  366. blog(LOG_WARNING, "Failed to parse custom config: %s",
  367. exception.what());
  368. throw MultitrackVideoError::critical(QTStr(
  369. "FailedToStartStream.InvalidCustomConfig"));
  370. }
  371. // copy unique ID from go live request
  372. if (go_live_config.has_value()) {
  373. parsed_custom.meta.config_id =
  374. go_live_config->meta.config_id;
  375. blog(LOG_INFO,
  376. "Using config_id from go live config with custom config: %s",
  377. parsed_custom.meta.config_id.c_str());
  378. }
  379. nlohmann::json custom_data = parsed_custom;
  380. blog(LOG_INFO, "Using custom go live config: %s",
  381. custom_data.dump(4).c_str());
  382. custom.emplace(std::move(parsed_custom));
  383. }
  384. if (go_live_config.has_value()) {
  385. blog(LOG_INFO, "Enhanced broadcasting config_id: '%s'",
  386. go_live_config->meta.config_id.c_str());
  387. }
  388. if (!go_live_config && !custom) {
  389. blog(LOG_ERROR,
  390. "MultitrackVideoOutput: no config set, this should never happen");
  391. throw MultitrackVideoError::warning(
  392. QTStr("FailedToStartStream.NoConfig"));
  393. }
  394. const auto &output_config = custom ? *custom : *go_live_config;
  395. const auto &service_config = go_live_config ? *go_live_config : *custom;
  396. std::vector<OBSEncoderAutoRelease> audio_encoders;
  397. std::shared_ptr<obs_encoder_group_t> video_encoder_group;
  398. auto outputs = SetupOBSOutput(parent, multitrack_video_name,
  399. dump_stream_to_file_config, output_config,
  400. audio_encoders, video_encoder_group,
  401. audio_encoder_id, main_audio_mixer,
  402. vod_track_mixer);
  403. auto output = std::move(outputs.output);
  404. auto recording_output = std::move(outputs.recording_output);
  405. if (!output)
  406. throw MultitrackVideoError::warning(
  407. QTStr("FailedToStartStream.FallbackToDefault")
  408. .arg(multitrack_video_name));
  409. auto multitrack_video_service =
  410. create_service(service_config, rtmp_url, stream_key);
  411. if (!multitrack_video_service)
  412. throw MultitrackVideoError::warning(
  413. QTStr("FailedToStartStream.FallbackToDefault")
  414. .arg(multitrack_video_name));
  415. obs_output_set_service(output, multitrack_video_service);
  416. OBSSignal start_streaming;
  417. OBSSignal stop_streaming;
  418. OBSSignal deactivate_stream;
  419. SetupSignalHandlers(false, this, output, start_streaming,
  420. stop_streaming, deactivate_stream);
  421. if (dump_stream_to_file_config && recording_output) {
  422. OBSSignal start_recording;
  423. OBSSignal stop_recording;
  424. OBSSignal deactivate_recording;
  425. SetupSignalHandlers(true, this, recording_output,
  426. start_recording, stop_recording,
  427. deactivate_recording);
  428. decltype(audio_encoders) recording_audio_encoders;
  429. recording_audio_encoders.reserve(audio_encoders.size());
  430. for (auto &encoder : audio_encoders) {
  431. recording_audio_encoders.emplace_back(
  432. obs_encoder_get_ref(encoder));
  433. }
  434. {
  435. const std::lock_guard current_stream_dump_lock{
  436. current_stream_dump_mutex};
  437. current_stream_dump.emplace(OBSOutputObjects{
  438. std::move(recording_output),
  439. video_encoder_group,
  440. std::move(recording_audio_encoders),
  441. nullptr,
  442. std::move(start_recording),
  443. std::move(stop_recording),
  444. std::move(deactivate_recording),
  445. });
  446. }
  447. }
  448. const std::lock_guard current_lock{current_mutex};
  449. current.emplace(OBSOutputObjects{
  450. std::move(output),
  451. video_encoder_group,
  452. std::move(audio_encoders),
  453. std::move(multitrack_video_service),
  454. std::move(start_streaming),
  455. std::move(stop_streaming),
  456. std::move(deactivate_stream),
  457. });
  458. }
  459. signal_handler_t *MultitrackVideoOutput::StreamingSignalHandler()
  460. {
  461. const std::lock_guard current_lock{current_mutex};
  462. return current.has_value()
  463. ? obs_output_get_signal_handler(current->output_)
  464. : nullptr;
  465. }
  466. void MultitrackVideoOutput::StartedStreaming()
  467. {
  468. OBSOutputAutoRelease dump_output;
  469. {
  470. const std::lock_guard current_stream_dump_lock{
  471. current_stream_dump_mutex};
  472. if (current_stream_dump && current_stream_dump->output_) {
  473. dump_output = obs_output_get_ref(
  474. current_stream_dump->output_);
  475. }
  476. }
  477. if (!dump_output)
  478. return;
  479. auto result = obs_output_start(dump_output);
  480. blog(LOG_INFO, "MultitrackVideoOutput: starting recording%s",
  481. result ? "" : " failed");
  482. }
  483. void MultitrackVideoOutput::StopStreaming()
  484. {
  485. OBSOutputAutoRelease current_output;
  486. {
  487. const std::lock_guard current_lock{current_mutex};
  488. if (current && current->output_)
  489. current_output = obs_output_get_ref(current->output_);
  490. }
  491. if (current_output)
  492. obs_output_stop(current_output);
  493. OBSOutputAutoRelease dump_output;
  494. {
  495. const std::lock_guard current_stream_dump_lock{
  496. current_stream_dump_mutex};
  497. if (current_stream_dump && current_stream_dump->output_)
  498. dump_output = obs_output_get_ref(
  499. current_stream_dump->output_);
  500. }
  501. if (dump_output)
  502. obs_output_stop(dump_output);
  503. }
  504. bool MultitrackVideoOutput::HandleIncompatibleSettings(
  505. QWidget *parent, config_t *config, obs_service_t *service,
  506. bool &useDelay, bool &enableNewSocketLoop, bool &enableDynBitrate)
  507. {
  508. QString incompatible_settings;
  509. QString where_to_disable;
  510. QString incompatible_settings_list;
  511. size_t num = 1;
  512. auto check_setting = [&](bool setting, const char *name,
  513. const char *section) {
  514. if (!setting)
  515. return;
  516. incompatible_settings +=
  517. QString(" %1. %2\n").arg(num).arg(QTStr(name));
  518. where_to_disable +=
  519. QString(" %1. [%2 → %3 → %4]\n")
  520. .arg(num)
  521. .arg(QTStr("Settings"))
  522. .arg(QTStr("Basic.Settings.Advanced"))
  523. .arg(QTStr(section));
  524. incompatible_settings_list += QString("%1, ").arg(name);
  525. num += 1;
  526. };
  527. check_setting(useDelay, "Basic.Settings.Advanced.StreamDelay",
  528. "Basic.Settings.Advanced.StreamDelay");
  529. #ifdef _WIN32
  530. check_setting(enableNewSocketLoop,
  531. "Basic.Settings.Advanced.Network.EnableNewSocketLoop",
  532. "Basic.Settings.Advanced.Network");
  533. #endif
  534. check_setting(enableDynBitrate,
  535. "Basic.Settings.Output.DynamicBitrate.Beta",
  536. "Basic.Settings.Advanced.Network");
  537. if (incompatible_settings.isEmpty())
  538. return true;
  539. OBSDataAutoRelease service_settings = obs_service_get_settings(service);
  540. QMessageBox mb(parent);
  541. mb.setIcon(QMessageBox::Critical);
  542. mb.setWindowTitle(QTStr("MultitrackVideo.IncompatibleSettings.Title"));
  543. mb.setText(QString(QTStr("MultitrackVideo.IncompatibleSettings.Text"))
  544. .arg(obs_data_get_string(service_settings,
  545. "multitrack_video_name"))
  546. .arg(incompatible_settings)
  547. .arg(where_to_disable));
  548. auto this_stream = mb.addButton(
  549. QTStr("MultitrackVideo.IncompatibleSettings.DisableAndStartStreaming"),
  550. QMessageBox::AcceptRole);
  551. auto all_streams = mb.addButton(
  552. QString(QTStr(
  553. "MultitrackVideo.IncompatibleSettings.UpdateAndStartStreaming")),
  554. QMessageBox::AcceptRole);
  555. mb.setStandardButtons(QMessageBox::StandardButton::Cancel);
  556. mb.exec();
  557. const char *action = "cancel";
  558. if (mb.clickedButton() == this_stream) {
  559. action = "DisableAndStartStreaming";
  560. } else if (mb.clickedButton() == all_streams) {
  561. action = "UpdateAndStartStreaming";
  562. }
  563. blog(LOG_INFO,
  564. "MultitrackVideoOutput: attempted to start stream with incompatible"
  565. "settings (%s); action taken: %s",
  566. incompatible_settings_list.toUtf8().constData(), action);
  567. if (mb.clickedButton() == this_stream ||
  568. mb.clickedButton() == all_streams) {
  569. useDelay = false;
  570. enableNewSocketLoop = false;
  571. enableDynBitrate = false;
  572. if (mb.clickedButton() == all_streams) {
  573. config_set_bool(config, "Output", "DelayEnable", false);
  574. #ifdef _WIN32
  575. config_set_bool(config, "Output", "NewSocketLoopEnable",
  576. false);
  577. #endif
  578. config_set_bool(config, "Output", "DynamicBitrate",
  579. false);
  580. }
  581. return true;
  582. }
  583. MultitrackVideoOutput::ReleaseOnMainThread(take_current());
  584. MultitrackVideoOutput::ReleaseOnMainThread(take_current_stream_dump());
  585. return false;
  586. }
  587. static bool
  588. create_video_encoders(const GoLiveApi::Config &go_live_config,
  589. std::shared_ptr<obs_encoder_group_t> &video_encoder_group,
  590. obs_output_t *output, obs_output_t *recording_output)
  591. {
  592. DStr video_encoder_name_buffer;
  593. if (go_live_config.encoder_configurations.empty()) {
  594. blog(LOG_WARNING,
  595. "MultitrackVideoOutput: Missing video encoder configurations");
  596. throw MultitrackVideoError::warning(
  597. QTStr("FailedToStartStream.MissingEncoderConfigs"));
  598. }
  599. std::shared_ptr<obs_encoder_group_t> encoder_group(
  600. obs_encoder_group_create(), obs_encoder_group_destroy);
  601. if (!encoder_group)
  602. return false;
  603. for (size_t i = 0; i < go_live_config.encoder_configurations.size();
  604. i++) {
  605. auto encoder = create_video_encoder(
  606. video_encoder_name_buffer, i,
  607. go_live_config.encoder_configurations[i]);
  608. if (!encoder)
  609. return false;
  610. if (!obs_encoder_set_group(encoder, encoder_group.get()))
  611. return false;
  612. obs_output_set_video_encoder2(output, encoder, i);
  613. if (recording_output)
  614. obs_output_set_video_encoder2(recording_output, encoder,
  615. i);
  616. }
  617. video_encoder_group = encoder_group;
  618. return true;
  619. }
  620. static void
  621. create_audio_encoders(const GoLiveApi::Config &go_live_config,
  622. std::vector<OBSEncoderAutoRelease> &audio_encoders,
  623. obs_output_t *output, obs_output_t *recording_output,
  624. const char *audio_encoder_id, size_t main_audio_mixer,
  625. std::optional<size_t> vod_track_mixer,
  626. std::vector<speaker_layout> &speaker_layouts,
  627. speaker_layout &current_layout)
  628. {
  629. speaker_layout speakers = SPEAKERS_UNKNOWN;
  630. obs_audio_info oai = {};
  631. if (obs_get_audio_info(&oai))
  632. speakers = oai.speakers;
  633. current_layout = speakers;
  634. auto sanitize_audio_channels = [&](obs_encoder_t *encoder,
  635. uint32_t channels) {
  636. speaker_layout target_speakers = SPEAKERS_UNKNOWN;
  637. for (size_t i = 0; i <= (size_t)SPEAKERS_7POINT1; i++) {
  638. if (get_audio_channels((speaker_layout)i) != channels)
  639. continue;
  640. target_speakers = (speaker_layout)i;
  641. break;
  642. }
  643. if (target_speakers == SPEAKERS_UNKNOWN) {
  644. blog(LOG_WARNING,
  645. "MultitrackVideoOutput: Could not find "
  646. "speaker layout for %" PRIu32 "channels "
  647. "while configuring encoder '%s'",
  648. channels, obs_encoder_get_name(encoder));
  649. return;
  650. }
  651. if (speakers != SPEAKERS_UNKNOWN &&
  652. (channels > get_audio_channels(speakers) ||
  653. speakers == target_speakers))
  654. return;
  655. auto it = std::find(std::begin(speaker_layouts),
  656. std::end(speaker_layouts), target_speakers);
  657. if (it == std::end(speaker_layouts))
  658. speaker_layouts.push_back(target_speakers);
  659. };
  660. using encoder_configs_type =
  661. decltype(go_live_config.audio_configurations.live);
  662. DStr encoder_name_buffer;
  663. size_t output_encoder_index = 0;
  664. auto create_encoders = [&](const char *name_prefix,
  665. const encoder_configs_type &configs,
  666. size_t mixer_idx) {
  667. if (configs.empty()) {
  668. blog(LOG_WARNING,
  669. "MultitrackVideoOutput: Missing audio encoder configurations (for '%s')",
  670. name_prefix);
  671. throw MultitrackVideoError::warning(QTStr(
  672. "FailedToStartStream.MissingEncoderConfigs"));
  673. }
  674. for (size_t i = 0; i < configs.size(); i++) {
  675. dstr_printf(encoder_name_buffer, "%s %zu", name_prefix,
  676. i);
  677. OBSDataAutoRelease settings = obs_data_create_from_json(
  678. configs[i].settings.dump().c_str());
  679. OBSEncoderAutoRelease audio_encoder =
  680. create_audio_encoder(encoder_name_buffer->array,
  681. audio_encoder_id, settings,
  682. mixer_idx);
  683. sanitize_audio_channels(audio_encoder,
  684. configs[i].channels);
  685. obs_output_set_audio_encoder(output, audio_encoder,
  686. output_encoder_index);
  687. if (recording_output)
  688. obs_output_set_audio_encoder(
  689. recording_output, audio_encoder,
  690. output_encoder_index);
  691. output_encoder_index += 1;
  692. audio_encoders.emplace_back(std::move(audio_encoder));
  693. }
  694. };
  695. create_encoders("multitrack video live audio",
  696. go_live_config.audio_configurations.live,
  697. main_audio_mixer);
  698. if (!vod_track_mixer.has_value())
  699. return;
  700. // we already check for empty inside of `create_encoders`
  701. encoder_configs_type empty = {};
  702. create_encoders("multitrack video vod audio",
  703. go_live_config.audio_configurations.vod.value_or(empty),
  704. *vod_track_mixer);
  705. return;
  706. }
  707. static const char *speaker_layout_to_string(speaker_layout layout)
  708. {
  709. switch (layout) {
  710. case SPEAKERS_MONO:
  711. return "Mono";
  712. case SPEAKERS_2POINT1:
  713. return "2.1";
  714. case SPEAKERS_4POINT0:
  715. return "4.0";
  716. case SPEAKERS_4POINT1:
  717. return "4.1";
  718. case SPEAKERS_5POINT1:
  719. return "5.1";
  720. case SPEAKERS_7POINT1:
  721. return "7.1";
  722. case SPEAKERS_UNKNOWN:
  723. case SPEAKERS_STEREO:
  724. return "Stereo";
  725. }
  726. return "Stereo";
  727. }
  728. static void handle_speaker_layout_issues(
  729. QWidget *parent, const QString &multitrack_video_name,
  730. const std::vector<speaker_layout> &requested_layouts,
  731. speaker_layout layout)
  732. {
  733. if (requested_layouts.empty())
  734. return;
  735. QString message;
  736. if (requested_layouts.size() == 1) {
  737. message =
  738. QTStr("MultitrackVideo.IncompatibleSettings.AudioChannelsSingle")
  739. .arg(QTStr(speaker_layout_to_string(
  740. requested_layouts.front())));
  741. } else {
  742. message =
  743. QTStr("MultitrackVideo.IncompatibleSettings.AudioChannelsMultiple")
  744. .arg(multitrack_video_name);
  745. }
  746. QMetaObject::invokeMethod(
  747. parent,
  748. [&] {
  749. QMessageBox mb(parent);
  750. mb.setIcon(QMessageBox::Critical);
  751. mb.setWindowTitle(QTStr(
  752. "MultitrackVideo.IncompatibleSettings.Title"));
  753. mb.setText(
  754. QTStr("MultitrackVideo.IncompatibleSettings.AudioChannels")
  755. .arg(multitrack_video_name)
  756. .arg(QTStr(speaker_layout_to_string(
  757. layout)))
  758. .arg(message));
  759. mb.setStandardButtons(
  760. QMessageBox::StandardButton::Cancel);
  761. mb.exec();
  762. },
  763. BlockingConnectionTypeFor(parent));
  764. blog(LOG_INFO,
  765. "MultitrackVideoOutput: Attempted to start stream with incompatible "
  766. "audio channel setting. Action taken: cancel");
  767. throw MultitrackVideoError::cancel();
  768. }
  769. static OBSOutputs
  770. SetupOBSOutput(QWidget *parent, const QString &multitrack_video_name,
  771. obs_data_t *dump_stream_to_file_config,
  772. const GoLiveApi::Config &go_live_config,
  773. std::vector<OBSEncoderAutoRelease> &audio_encoders,
  774. std::shared_ptr<obs_encoder_group_t> &video_encoder_group,
  775. const char *audio_encoder_id, size_t main_audio_mixer,
  776. std::optional<size_t> vod_track_mixer)
  777. {
  778. auto output = create_output();
  779. OBSOutputAutoRelease recording_output;
  780. if (dump_stream_to_file_config)
  781. recording_output =
  782. create_recording_output(dump_stream_to_file_config);
  783. if (!create_video_encoders(go_live_config, video_encoder_group, output,
  784. recording_output))
  785. return {nullptr, nullptr};
  786. std::vector<speaker_layout> requested_speaker_layouts;
  787. speaker_layout current_layout = SPEAKERS_UNKNOWN;
  788. create_audio_encoders(go_live_config, audio_encoders, output,
  789. recording_output, audio_encoder_id,
  790. main_audio_mixer, vod_track_mixer,
  791. requested_speaker_layouts, current_layout);
  792. handle_speaker_layout_issues(parent, multitrack_video_name,
  793. requested_speaker_layouts, current_layout);
  794. return {std::move(output), std::move(recording_output)};
  795. }
  796. void SetupSignalHandlers(bool recording, MultitrackVideoOutput *self,
  797. obs_output_t *output, OBSSignal &start,
  798. OBSSignal &stop, OBSSignal &deactivate)
  799. {
  800. auto handler = obs_output_get_signal_handler(output);
  801. if (recording)
  802. start.Connect(handler, "start", RecordingStartHandler, self);
  803. stop.Connect(handler, "stop",
  804. !recording ? StreamStopHandler : RecordingStopHandler,
  805. self);
  806. deactivate.Connect(handler, "deactivate",
  807. !recording ? StreamDeactivateHandler
  808. : RecordingDeactivateHandler,
  809. self);
  810. }
  811. std::optional<MultitrackVideoOutput::OBSOutputObjects>
  812. MultitrackVideoOutput::take_current()
  813. {
  814. const std::lock_guard<std::mutex> current_lock{current_mutex};
  815. auto val = std::move(current);
  816. current.reset();
  817. return val;
  818. }
  819. std::optional<MultitrackVideoOutput::OBSOutputObjects>
  820. MultitrackVideoOutput::take_current_stream_dump()
  821. {
  822. const std::lock_guard<std::mutex> current_stream_dump_lock{
  823. current_stream_dump_mutex};
  824. auto val = std::move(current_stream_dump);
  825. current_stream_dump.reset();
  826. return val;
  827. }
  828. void MultitrackVideoOutput::ReleaseOnMainThread(
  829. std::optional<OBSOutputObjects> objects)
  830. {
  831. if (!objects.has_value())
  832. return;
  833. QMetaObject::invokeMethod(
  834. QApplication::instance()->thread(),
  835. [objects = std::move(objects)] {}, Qt::QueuedConnection);
  836. }
  837. void StreamStopHandler(void *arg, calldata_t *params)
  838. {
  839. auto self = static_cast<MultitrackVideoOutput *>(arg);
  840. OBSOutputAutoRelease stream_dump_output;
  841. {
  842. const std::lock_guard<std::mutex> current_stream_dump_lock{
  843. self->current_stream_dump_mutex};
  844. if (self->current_stream_dump &&
  845. self->current_stream_dump->output_)
  846. stream_dump_output = obs_output_get_ref(
  847. self->current_stream_dump->output_);
  848. }
  849. if (stream_dump_output)
  850. obs_output_stop(stream_dump_output);
  851. if (obs_output_active(static_cast<obs_output_t *>(
  852. calldata_ptr(params, "output"))))
  853. return;
  854. MultitrackVideoOutput::ReleaseOnMainThread(self->take_current());
  855. }
  856. void StreamDeactivateHandler(void *arg, calldata_t *params)
  857. {
  858. auto self = static_cast<MultitrackVideoOutput *>(arg);
  859. if (obs_output_reconnecting(static_cast<obs_output_t *>(
  860. calldata_ptr(params, "output"))))
  861. return;
  862. MultitrackVideoOutput::ReleaseOnMainThread(self->take_current());
  863. }
  864. void RecordingStartHandler(void * /* arg */, calldata_t * /* data */)
  865. {
  866. blog(LOG_INFO, "MultitrackVideoOutput: recording started");
  867. }
  868. void RecordingStopHandler(void *arg, calldata_t *params)
  869. {
  870. auto self = static_cast<MultitrackVideoOutput *>(arg);
  871. blog(LOG_INFO, "MultitrackVideoOutput: recording stopped");
  872. if (obs_output_active(static_cast<obs_output_t *>(
  873. calldata_ptr(params, "output"))))
  874. return;
  875. MultitrackVideoOutput::ReleaseOnMainThread(
  876. self->take_current_stream_dump());
  877. }
  878. void RecordingDeactivateHandler(void *arg, calldata_t * /*data*/)
  879. {
  880. auto self = static_cast<MultitrackVideoOutput *>(arg);
  881. MultitrackVideoOutput::ReleaseOnMainThread(
  882. self->take_current_stream_dump());
  883. }